Skip to content

Commit d43621c

Browse files
authored
Support extra docker configs (#360)
* Get additional docker configs in local deployment commands. This accepts only device_requests for now. * Update new docker extra config option documentation * Support mounting volumes using extra docker configs * Update readme * Update readme
1 parent 92e5987 commit d43621c

File tree

11 files changed

+344
-26
lines changed

11 files changed

+344
-26
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ Options:
160160
--python-venv TEXT The path of the python virtual environment to be used
161161
--update Pull the LEAN engine image before running the backtest
162162
--backtest-name TEXT Backtest name
163+
--extra-docker-config TEXT Extra docker configuration as a JSON string. For more information https://docker-
164+
py.readthedocs.io/en/stable/containers.html
163165
--no-update Use the local LEAN engine image instead of pulling the latest version
164166
--lean-config FILE The Lean configuration file that should be used (defaults to the nearest lean.json)
165167
--verbose Enable debug logging
@@ -1098,6 +1100,8 @@ Options:
10981100
holdings
10991101
--update Pull the LEAN engine image before starting live trading
11001102
--show-secrets Show secrets as they are input
1103+
--extra-docker-config TEXT Extra docker configuration as a JSON string. For more information https://docker-
1104+
py.readthedocs.io/en/stable/containers.html
11011105
--no-update Use the local LEAN engine image instead of pulling the latest version
11021106
--lean-config FILE The Lean configuration file that should be used (defaults to the nearest lean.json)
11031107
--verbose Enable debug logging
@@ -1383,6 +1387,8 @@ Options:
13831387
--estimate Estimate optimization runtime without running it
13841388
--max-concurrent-backtests INTEGER RANGE
13851389
Maximum number of concurrent backtests to run [x>=1]
1390+
--extra-docker-config TEXT Extra docker configuration as a JSON string. For more information https://docker-
1391+
py.readthedocs.io/en/stable/containers.html
13861392
--no-update Use the local LEAN engine image instead of pulling the latest version
13871393
--lean-config FILE The Lean configuration file that should be used (defaults to the nearest lean.json)
13881394
--verbose Enable debug logging
@@ -1505,6 +1511,8 @@ Options:
15051511
--no-open Don't open the Jupyter Lab environment in the browser after starting it
15061512
--image TEXT The LEAN research image to use (defaults to quantconnect/research:latest)
15071513
--update Pull the LEAN research image before starting the research environment
1514+
--extra-docker-config TEXT Extra docker configuration as a JSON string. For more information https://docker-
1515+
py.readthedocs.io/en/stable/containers.html
15081516
--no-update Use the local LEAN research image instead of pulling the latest version
15091517
--lean-config FILE The Lean configuration file that should be used (defaults to the nearest lean.json)
15101518
--verbose Enable debug logging

lean/commands/backtest.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
# See the License for the specific language governing permissions and
1212
# limitations under the License.
1313

14-
1514
from pathlib import Path
1615
from typing import List, Optional, Tuple
1716
from click import command, option, argument, Choice
@@ -289,6 +288,11 @@ def _select_organization() -> QCMinimalOrganization:
289288
type=(str, str),
290289
multiple=True,
291290
hidden=True)
291+
@option("--extra-docker-config",
292+
type=str,
293+
default="{}",
294+
help="Extra docker configuration as a JSON string. "
295+
"For more information https://docker-py.readthedocs.io/en/stable/containers.html")
292296
@option("--no-update",
293297
is_flag=True,
294298
default=False,
@@ -307,6 +311,7 @@ def backtest(project: Path,
307311
backtest_name: str,
308312
addon_module: Optional[List[str]],
309313
extra_config: Optional[Tuple[str, str]],
314+
extra_docker_config: Optional[str],
310315
no_update: bool,
311316
**kwargs) -> None:
312317
"""Backtest a project locally using Docker.
@@ -324,6 +329,8 @@ def backtest(project: Path,
324329
Alternatively you can set the default engine image for all commands using `lean config set engine-image <image>`.
325330
"""
326331
from datetime import datetime
332+
from json import loads
333+
327334
logger = container.logger
328335
project_manager = container.project_manager
329336
algorithm_file = project_manager.find_algorithm_file(Path(project))
@@ -407,4 +414,5 @@ def backtest(project: Path,
407414
engine_image,
408415
debugging_method,
409416
release,
410-
detach)
417+
detach,
418+
loads(extra_docker_config))

lean/commands/live/deploy.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ def _get_default_value(key: str) -> Optional[Any]:
255255
type=(str, str),
256256
multiple=True,
257257
hidden=True)
258+
@option("--extra-docker-config",
259+
type=str,
260+
default="{}",
261+
help="Extra docker configuration as a JSON string. "
262+
"For more information https://docker-py.readthedocs.io/en/stable/containers.html")
258263
@option("--no-update",
259264
is_flag=True,
260265
default=False,
@@ -275,6 +280,7 @@ def deploy(project: Path,
275280
show_secrets: bool,
276281
addon_module: Optional[List[str]],
277282
extra_config: Optional[Tuple[str, str]],
283+
extra_docker_config: Optional[str],
278284
no_update: bool,
279285
**kwargs) -> None:
280286
"""Start live trading a project locally using Docker.
@@ -299,6 +305,7 @@ def deploy(project: Path,
299305
"""
300306
from copy import copy
301307
from datetime import datetime
308+
from json import loads
302309
# Reset globals so we reload everything in between tests
303310
global _cached_lean_config
304311
_cached_lean_config = None
@@ -430,4 +437,4 @@ def deploy(project: Path,
430437
raise RuntimeError(f"InteractiveBrokers is currently not supported for ARM hosts")
431438

432439
lean_runner = container.lean_runner
433-
lean_runner.run_lean(lean_config, environment_name, algorithm_file, output, engine_image, None, release, detach)
440+
lean_runner.run_lean(lean_config, environment_name, algorithm_file, output, engine_image, None, release, detach, loads(extra_docker_config))

lean/commands/optimize.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from click import command, argument, option, Choice, IntRange
1919

2020
from lean.click import LeanCommand, PathParameter, ensure_options
21+
from lean.components.docker.lean_runner import LeanRunner
2122
from lean.constants import DEFAULT_ENGINE_IMAGE
2223
from lean.container import container
2324
from lean.models.api import QCParameter, QCBacktest
@@ -119,6 +120,11 @@ def get_filename_timestamp(path: Path) -> datetime:
119120
type=(str, str),
120121
multiple=True,
121122
hidden=True)
123+
@option("--extra-docker-config",
124+
type=str,
125+
default="{}",
126+
help="Extra docker configuration as a JSON string. "
127+
"For more information https://docker-py.readthedocs.io/en/stable/containers.html")
122128
@option("--no-update",
123129
is_flag=True,
124130
default=False,
@@ -139,6 +145,7 @@ def optimize(project: Path,
139145
max_concurrent_backtests: Optional[int],
140146
addon_module: Optional[List[str]],
141147
extra_config: Optional[Tuple[str, str]],
148+
extra_docker_config: Optional[str],
142149
no_update: bool) -> None:
143150
"""Optimize a project's parameters locally using Docker.
144151
@@ -308,6 +315,9 @@ def optimize(project: Path,
308315
)
309316
container.update_manager.pull_docker_image_if_necessary(engine_image, update, no_update)
310317

318+
# Add known additional run options from the extra docker config
319+
LeanRunner.parse_extra_docker_config(run_options, loads(extra_docker_config))
320+
311321
project_manager.copy_code(algorithm_file.parent, output / "code")
312322

313323
success = container.docker_manager.run_image(engine_image, **run_options)

lean/commands/research.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from typing import Optional, Tuple
1616
from click import command, argument, option, Choice
1717
from lean.click import LeanCommand, PathParameter
18+
from lean.components.docker.lean_runner import LeanRunner
1819
from lean.constants import DEFAULT_RESEARCH_IMAGE, LEAN_ROOT_PATH
1920
from lean.container import container
2021
from lean.models.data_providers import QuantConnectDataProvider, all_data_providers
@@ -65,6 +66,11 @@ def _check_docker_output(chunk: str, port: int) -> None:
6566
type=(str, str),
6667
multiple=True,
6768
hidden=True)
69+
@option("--extra-docker-config",
70+
type=str,
71+
default="{}",
72+
help="Extra docker configuration as a JSON string. "
73+
"For more information https://docker-py.readthedocs.io/en/stable/containers.html")
6874
@option("--no-update",
6975
is_flag=True,
7076
default=False,
@@ -79,6 +85,7 @@ def research(project: Path,
7985
image: Optional[str],
8086
update: bool,
8187
extra_config: Optional[Tuple[str, str]],
88+
extra_docker_config: Optional[str],
8289
no_update: bool,
8390
**kwargs) -> None:
8491
"""Run a Jupyter Lab environment locally using Docker.
@@ -89,6 +96,7 @@ def research(project: Path,
8996
"""
9097
from docker.types import Mount
9198
from docker.errors import APIError
99+
from json import loads
92100

93101
logger = container.logger
94102

@@ -116,7 +124,7 @@ def research(project: Path,
116124
# Set extra config
117125
for key, value in extra_config:
118126
lean_config[key] = value
119-
127+
120128
run_options = lean_runner.get_basic_docker_config(lean_config,
121129
algorithm_file,
122130
temp_manager.create_temporary_directory(),
@@ -160,6 +168,9 @@ def research(project: Path,
160168
# Run the script that starts Jupyter Lab when all set up has been done
161169
run_options["commands"].append("./start.sh")
162170

171+
# Add known additional run options from the extra docker config
172+
LeanRunner.parse_extra_docker_config(run_options, loads(extra_docker_config))
173+
163174
project_config_manager = container.project_config_manager
164175
cli_config_manager = container.cli_config_manager
165176

lean/components/docker/lean_runner.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from pathlib import Path
1515
from typing import Any, Dict, Optional, List
16+
1617
from lean.components.cloud.module_manager import ModuleManager
1718
from lean.components.config.lean_config_manager import LeanConfigManager
1819
from lean.components.config.output_config_manager import OutputConfigManager
@@ -70,7 +71,8 @@ def run_lean(self,
7071
image: DockerImage,
7172
debugging_method: Optional[DebuggingMethod],
7273
release: bool,
73-
detach: bool) -> None:
74+
detach: bool,
75+
extra_docker_config: Optional[Dict[str, Any]] = None) -> None:
7476
"""Runs the LEAN engine locally in Docker.
7577
7678
Raises an error if something goes wrong.
@@ -83,6 +85,7 @@ def run_lean(self,
8385
:param debugging_method: the debugging method if debugging needs to be enabled, None if not
8486
:param release: whether C# projects should be compiled in release configuration instead of debug
8587
:param detach: whether LEAN should run in a detached container
88+
:param extra_docker_config: additional docker configurations
8689
"""
8790
project_dir = algorithm_file.parent
8891

@@ -95,6 +98,9 @@ def run_lean(self,
9598
release,
9699
detach)
97100

101+
# Add known additional run options from the extra docker config
102+
self.parse_extra_docker_config(run_options, extra_docker_config)
103+
98104
# Set up PTVSD debugging
99105
if debugging_method == DebuggingMethod.PTVSD:
100106
run_options["ports"]["5678"] = "5678"
@@ -762,3 +768,19 @@ def mount_project_and_library_directories(self, project_dir: Path, run_options:
762768
"bind": "/Library",
763769
"mode": "rw"
764770
}
771+
772+
@staticmethod
773+
def parse_extra_docker_config(run_options: Dict[str, Any], extra_docker_config: Optional[Dict[str, Any]]) -> None:
774+
from docker.types import DeviceRequest
775+
# Add known additional run options from the extra docker config.
776+
# For now, only device_requests is supported
777+
if extra_docker_config is not None:
778+
if "device_requests" in extra_docker_config:
779+
run_options["device_requests"] = [DeviceRequest(**device_request)
780+
for device_request in extra_docker_config["device_requests"]]
781+
782+
if "volumes" in extra_docker_config:
783+
volumes = run_options.get("volumes")
784+
if not volumes:
785+
volumes = run_options["volumes"] = {}
786+
volumes.update(extra_docker_config["volumes"])

tests/commands/test_backtest.py

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ def test_backtest_calls_lean_runner_with_correct_algorithm_file() -> None:
5757
ENGINE_IMAGE,
5858
None,
5959
False,
60-
False)
60+
False,
61+
{})
6162

6263

6364
def test_backtest_calls_lean_runner_with_default_output_directory() -> None:
@@ -88,7 +89,8 @@ def test_backtest_calls_lean_runner_with_custom_output_directory() -> None:
8889
ENGINE_IMAGE,
8990
None,
9091
False,
91-
False)
92+
False,
93+
{})
9294

9395

9496
def test_backtest_calls_lean_runner_with_release_mode() -> None:
@@ -105,7 +107,8 @@ def test_backtest_calls_lean_runner_with_release_mode() -> None:
105107
ENGINE_IMAGE,
106108
None,
107109
True,
108-
False)
110+
False,
111+
{})
109112

110113

111114
def test_backtest_calls_lean_runner_with_detach() -> None:
@@ -122,7 +125,8 @@ def test_backtest_calls_lean_runner_with_detach() -> None:
122125
ENGINE_IMAGE,
123126
None,
124127
False,
125-
True)
128+
True,
129+
{})
126130

127131

128132
def test_backtest_aborts_when_project_does_not_exist() -> None:
@@ -163,7 +167,8 @@ def test_backtest_forces_update_when_update_option_given() -> None:
163167
ENGINE_IMAGE,
164168
None,
165169
False,
166-
False)
170+
False,
171+
{})
167172

168173

169174
def test_backtest_passes_custom_image_to_lean_runner_when_set_in_config() -> None:
@@ -182,7 +187,8 @@ def test_backtest_passes_custom_image_to_lean_runner_when_set_in_config() -> Non
182187
DockerImage(name="custom/lean", tag="123"),
183188
None,
184189
False,
185-
False)
190+
False,
191+
{})
186192

187193

188194
def test_backtest_passes_custom_image_to_lean_runner_when_given_as_option() -> None:
@@ -201,7 +207,8 @@ def test_backtest_passes_custom_image_to_lean_runner_when_given_as_option() -> N
201207
DockerImage(name="custom/lean", tag="456"),
202208
None,
203209
False,
204-
False)
210+
False,
211+
{})
205212

206213

207214
@pytest.mark.parametrize("python_venv", ["Custom-venv",
@@ -289,7 +296,8 @@ def test_backtest_passes_correct_debugging_method_to_lean_runner(value: str, deb
289296
ENGINE_IMAGE,
290297
debugging_method,
291298
False,
292-
False)
299+
False,
300+
{})
293301

294302

295303
def test_backtest_auto_updates_outdated_python_pycharm_debug_config() -> None:
@@ -649,3 +657,31 @@ def test_backtest_adds_python_libraries_path_to_lean_config() -> None:
649657
expected_library_path = (Path("/") / library_path.relative_to(lean_cli_root_dir)).as_posix()
650658

651659
assert expected_library_path in lean_config.get('python-additional-paths')
660+
661+
662+
def test_backtest_calls_lean_runner_with_extra_docker_config() -> None:
663+
create_fake_lean_cli_directory()
664+
665+
result = CliRunner().invoke(lean, ["backtest", "Python Project",
666+
"--extra-docker-config",
667+
'{"device_requests": [{"count": -1, "capabilities": [["compute"]]}],'
668+
'"volumes": {"extra/path": {"bind": "/extra/path", "mode": "rw"}}}'])
669+
670+
assert result.exit_code == 0
671+
672+
container.lean_runner.run_lean.assert_called_once_with(mock.ANY,
673+
"backtesting",
674+
Path("Python Project/main.py").resolve(),
675+
mock.ANY,
676+
ENGINE_IMAGE,
677+
None,
678+
False,
679+
False,
680+
{
681+
"device_requests": [
682+
{"count": -1, "capabilities": [["compute"]]}
683+
],
684+
"volumes": {
685+
"extra/path": {"bind": "/extra/path", "mode": "rw"}
686+
}
687+
})

0 commit comments

Comments
 (0)