From e9957048a4c30b65809026b7712a84b8b2642307 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Sat, 31 Oct 2026 06:32:16 -0400 Subject: [PATCH 1/3] fix: apply quant bridge host rewrite and file mounts to selected environment lean live deploy stores brokerage settings under lean_config["environments"][], so the top-level-only checks in LeanRunner never saw them. Co-Authored-By: Claude Sonnet 5.5 --- lean/components/docker/lean_runner.py | 59 +++++++++++++-------- tests/components/docker/test_lean_runner.py | 52 ++++++++++++++++++ 2 files changed, 88 insertions(+), 23 deletions(-) diff --git a/lean/components/docker/lean_runner.py b/lean/components/docker/lean_runner.py index e1f08a70..b38008a0 100644 --- a/lean/components/docker/lean_runner.py +++ b/lean/components/docker/lean_runner.py @@ -225,12 +225,13 @@ def get_basic_docker_config(self, # Update all hosts that need to point to the host's localhost to host.docker.internal so they resolve properly # TODO: we should remove it or add to config json - for key in ["quant-bridge-server-host"]: - if key not in lean_config: - continue + for config in self._get_config_scopes(lean_config): + for key in ["quant-bridge-server-host"]: + if key not in config: + continue - if lean_config[key] == "localhost" or lean_config[key] == "127.0.0.1": - lean_config[key] = "host.docker.internal" + if config[key] == "localhost" or config[key] == "127.0.0.1": + config[key] = "host.docker.internal" # Set up modules set_up_common_csharp_options_called = self._setup_installed_packages(run_options, image, lean_config) @@ -287,12 +288,13 @@ def get_basic_docker_config_without_algo(self, # Update all hosts that need to point to the host's localhost to host.docker.internal so they resolve properly # TODO: we should remove it or add to config json - for key in ["quant-bridge-server-host"]: - if key not in lean_config: - continue + for config in self._get_config_scopes(lean_config): + for key in ["quant-bridge-server-host"]: + if key not in config: + continue - if lean_config[key] == "localhost" or lean_config[key] == "127.0.0.1": - lean_config[key] = "host.docker.internal" + if config[key] == "localhost" or config[key] == "127.0.0.1": + config[key] = "host.docker.internal" # Set up modules self._setup_installed_packages(run_options, image, lean_config, target_path) @@ -415,22 +417,33 @@ def _mount_common_directories(self, ("transaction-log", cli_root_dir), ("quant-bridge-symbol-map-file", cli_root_dir / DEFAULT_DATA_DIRECTORY_NAME / "symbol-properties") ] - for key, base_path in files_to_mount: - if key not in lean_config or lean_config[key] == "": - continue + for config in self._get_config_scopes(lean_config): + for key, base_path in files_to_mount: + if key not in config or config[key] == "": + continue - lean_config_entry = Path(lean_config[key]) - local_path = lean_config_entry if lean_config_entry.is_absolute() else base_path / lean_config_entry - if not local_path.exists(): - local_path.parent.mkdir(parents=True, exist_ok=True) - local_path.touch() + lean_config_entry = Path(config[key]) + local_path = lean_config_entry if lean_config_entry.is_absolute() else base_path / lean_config_entry + if not local_path.exists(): + local_path.parent.mkdir(parents=True, exist_ok=True) + local_path.touch() - run_options["mounts"].append(Mount(target=f"/Files/{key}", - source=str(local_path), - type="bind", - read_only=False)) + run_options["mounts"].append(Mount(target=f"/Files/{key}", + source=str(local_path), + type="bind", + read_only=False)) - lean_config[key] = f"/Files/{key}" + config[key] = f"/Files/{key}" + + @staticmethod + def _get_config_scopes(lean_config: Dict[str, Any]) -> List[Dict[str, Any]]: + """Returns the top level of the Lean config plus the selected environment's entries, if any.""" + scopes = [lean_config] + if "environment" in lean_config and "environments" in lean_config: + environment = lean_config["environments"].get(lean_config["environment"]) + if environment is not None: + scopes.append(environment) + return scopes def _initialize_run_options(self, detach: bool, docker_project_config: Dict[str, Any], debugging_method: Optional[DebuggingMethod]): diff --git a/tests/components/docker/test_lean_runner.py b/tests/components/docker/test_lean_runner.py index 26d009f8..ac7dd735 100644 --- a/tests/components/docker/test_lean_runner.py +++ b/tests/components/docker/test_lean_runner.py @@ -831,3 +831,55 @@ def test_lean_runner_parses_environment_values_containing_equals_from_extra_dock {"environment": ["JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8", "TOKEN=abc=="]}) assert run_options["environment"] == {"JAVA_TOOL_OPTIONS": "-Dfile.encoding=UTF-8", "TOKEN": "abc=="} + + +def _run_with_environment_config(lean_config: dict): + create_fake_lean_cli_directory() + + docker_manager = mock.Mock() + docker_manager.run_image.return_value = True + + lean_runner = create_lean_runner(docker_manager) + lean_runner.run_lean(lean_config, + "lean-cli", + Path.cwd() / "Python Project" / "main.py", + Path.cwd() / "output", + ENGINE_IMAGE, + None, + False, + False) + + docker_manager.run_image.assert_called_once() + return docker_manager.run_image.call_args + + +@pytest.mark.parametrize("host", ["localhost", "127.0.0.1"]) +def test_run_lean_rewrites_localhost_quant_bridge_host_in_selected_environment(host: str) -> None: + lean_config = { + "environment": "lean-cli", + "environments": {"lean-cli": {"quant-bridge-server-host": host}} + } + + _run_with_environment_config(lean_config) + + assert lean_config["environments"]["lean-cli"]["quant-bridge-server-host"] == "host.docker.internal" + + +def test_run_lean_mounts_quant_bridge_symbol_map_file_from_selected_environment() -> None: + lean_config = { + "environment": "lean-cli", + "environments": {"lean-cli": {"quant-bridge-symbol-map-file": "quant-bridge-symbol-map.json"}} + } + + args, kwargs = _run_with_environment_config(lean_config) + + from lean.container import container + cli_root_dir = container.lean_config_manager.get_cli_root_directory() + expected_source = cli_root_dir / DEFAULT_DATA_DIRECTORY_NAME / "symbol-properties" / "quant-bridge-symbol-map.json" + + assert any([ + Path(mount["Source"]) == expected_source and + mount["Target"] == "/Files/quant-bridge-symbol-map-file" + for mount in kwargs["mounts"] + ]) + assert lean_config["environments"]["lean-cli"]["quant-bridge-symbol-map-file"] == "/Files/quant-bridge-symbol-map-file" From be0e96b9858944089d273cbf23382f70971c5115 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Sat, 31 Oct 2026 06:34:37 -0400 Subject: [PATCH 2/3] fix: mount each lean config file once when set at root and environment Co-Authored-By: Claude Sonnet 5.5 --- lean/components/docker/lean_runner.py | 30 ++++++++++++--------- tests/components/docker/test_lean_runner.py | 20 ++++++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/lean/components/docker/lean_runner.py b/lean/components/docker/lean_runner.py index b38008a0..5c704eb4 100644 --- a/lean/components/docker/lean_runner.py +++ b/lean/components/docker/lean_runner.py @@ -417,22 +417,26 @@ def _mount_common_directories(self, ("transaction-log", cli_root_dir), ("quant-bridge-symbol-map-file", cli_root_dir / DEFAULT_DATA_DIRECTORY_NAME / "symbol-properties") ] - for config in self._get_config_scopes(lean_config): - for key, base_path in files_to_mount: - if key not in config or config[key] == "": - continue + scopes = self._get_config_scopes(lean_config) + for key, base_path in files_to_mount: + # The same key can be set at the top level and in the selected environment: + # mount it once, with the environment's value taking precedence + configs = [config for config in scopes if key in config and config[key] != ""] + if not configs: + continue - lean_config_entry = Path(config[key]) - local_path = lean_config_entry if lean_config_entry.is_absolute() else base_path / lean_config_entry - if not local_path.exists(): - local_path.parent.mkdir(parents=True, exist_ok=True) - local_path.touch() + lean_config_entry = Path(configs[-1][key]) + local_path = lean_config_entry if lean_config_entry.is_absolute() else base_path / lean_config_entry + if not local_path.exists(): + local_path.parent.mkdir(parents=True, exist_ok=True) + local_path.touch() - run_options["mounts"].append(Mount(target=f"/Files/{key}", - source=str(local_path), - type="bind", - read_only=False)) + run_options["mounts"].append(Mount(target=f"/Files/{key}", + source=str(local_path), + type="bind", + read_only=False)) + for config in configs: config[key] = f"/Files/{key}" @staticmethod diff --git a/tests/components/docker/test_lean_runner.py b/tests/components/docker/test_lean_runner.py index ac7dd735..b463bcf3 100644 --- a/tests/components/docker/test_lean_runner.py +++ b/tests/components/docker/test_lean_runner.py @@ -883,3 +883,23 @@ def test_run_lean_mounts_quant_bridge_symbol_map_file_from_selected_environment( for mount in kwargs["mounts"] ]) assert lean_config["environments"]["lean-cli"]["quant-bridge-symbol-map-file"] == "/Files/quant-bridge-symbol-map-file" + + +def test_run_lean_mounts_quant_bridge_symbol_map_file_once_when_set_in_root_and_environment() -> None: + lean_config = { + "environment": "lean-cli", + "quant-bridge-symbol-map-file": "root-symbol-map.json", + "environments": {"lean-cli": {"quant-bridge-symbol-map-file": "env-symbol-map.json"}} + } + + args, kwargs = _run_with_environment_config(lean_config) + + from lean.container import container + cli_root_dir = container.lean_config_manager.get_cli_root_directory() + expected_source = cli_root_dir / DEFAULT_DATA_DIRECTORY_NAME / "symbol-properties" / "env-symbol-map.json" + + targets = [mount["Target"] for mount in kwargs["mounts"]] + assert targets.count("/Files/quant-bridge-symbol-map-file") == 1 + assert any(Path(mount["Source"]) == expected_source for mount in kwargs["mounts"]) + assert lean_config["quant-bridge-symbol-map-file"] == "/Files/quant-bridge-symbol-map-file" + assert lean_config["environments"]["lean-cli"]["quant-bridge-symbol-map-file"] == "/Files/quant-bridge-symbol-map-file" From 2210781d95e617cb85ffac7a777328ec15475f24 Mon Sep 17 00:00:00 2001 From: Romazes Date: Mon, 2 Nov 2026 16:31:32 +0200 Subject: [PATCH 3/3] refactor: extract the localhost rewrite and log the config changes - the localhost to host.docker.internal rewrite was copied in both docker config builders, so a later fix could land in one and miss the other - the method takes the config and returns it, so the change is visible at the call site instead of hidden inside a helper - debug lines say which host was replaced and which file was mounted where, for live deploy troubleshooting --- lean/components/docker/lean_runner.py | 47 +++++++++++++-------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/lean/components/docker/lean_runner.py b/lean/components/docker/lean_runner.py index 5c704eb4..fdd5eb9e 100644 --- a/lean/components/docker/lean_runner.py +++ b/lean/components/docker/lean_runner.py @@ -223,15 +223,7 @@ def get_basic_docker_config(self, self._mount_common_directories(run_options, paths_to_mount, lean_config, data_dir, storage_dir, project_dir, output_dir) - # Update all hosts that need to point to the host's localhost to host.docker.internal so they resolve properly - # TODO: we should remove it or add to config json - for config in self._get_config_scopes(lean_config): - for key in ["quant-bridge-server-host"]: - if key not in config: - continue - - if config[key] == "localhost" or config[key] == "127.0.0.1": - config[key] = "host.docker.internal" + lean_config = self._replace_localhost_with_docker_host(lean_config) # Set up modules set_up_common_csharp_options_called = self._setup_installed_packages(run_options, image, lean_config) @@ -286,15 +278,7 @@ def get_basic_docker_config_without_algo(self, self._mount_common_directories(run_options, paths_to_mount, lean_config, data_dir, storage_dir, None, None) - # Update all hosts that need to point to the host's localhost to host.docker.internal so they resolve properly - # TODO: we should remove it or add to config json - for config in self._get_config_scopes(lean_config): - for key in ["quant-bridge-server-host"]: - if key not in config: - continue - - if config[key] == "localhost" or config[key] == "127.0.0.1": - config[key] = "host.docker.internal" + lean_config = self._replace_localhost_with_docker_host(lean_config) # Set up modules self._setup_installed_packages(run_options, image, lean_config, target_path) @@ -417,11 +401,11 @@ def _mount_common_directories(self, ("transaction-log", cli_root_dir), ("quant-bridge-symbol-map-file", cli_root_dir / DEFAULT_DATA_DIRECTORY_NAME / "symbol-properties") ] - scopes = self._get_config_scopes(lean_config) + top_level_and_environment = self._get_top_level_and_selected_environment(lean_config) for key, base_path in files_to_mount: # The same key can be set at the top level and in the selected environment: # mount it once, with the environment's value taking precedence - configs = [config for config in scopes if key in config and config[key] != ""] + configs = [config for config in top_level_and_environment if key in config and config[key] != ""] if not configs: continue @@ -436,18 +420,33 @@ def _mount_common_directories(self, type="bind", read_only=False)) + self._logger.debug( + f"LeanRunner._mount_common_directories(): mounting '{local_path}' to '/Files/{key}', " + f"setting '{key}' to '/Files/{key}'") for config in configs: config[key] = f"/Files/{key}" + def _replace_localhost_with_docker_host(self, lean_config: Dict[str, Any]) -> Dict[str, Any]: + """Returns the config with hosts that point to the local machine set to host.docker.internal.""" + # TODO: we should remove it or add to config json + for config in self._get_top_level_and_selected_environment(lean_config): + for key in ["quant-bridge-server-host"]: + if key in config and config[key] in ["localhost", "127.0.0.1"]: + self._logger.debug( + f"LeanRunner._replace_localhost_with_docker_host(): replacing '{key}' value '{config[key]}' " + f"with 'host.docker.internal'") + config[key] = "host.docker.internal" + return lean_config + @staticmethod - def _get_config_scopes(lean_config: Dict[str, Any]) -> List[Dict[str, Any]]: + def _get_top_level_and_selected_environment(lean_config: Dict[str, Any]) -> List[Dict[str, Any]]: """Returns the top level of the Lean config plus the selected environment's entries, if any.""" - scopes = [lean_config] + top_level_and_environment = [lean_config] if "environment" in lean_config and "environments" in lean_config: environment = lean_config["environments"].get(lean_config["environment"]) if environment is not None: - scopes.append(environment) - return scopes + top_level_and_environment.append(environment) + return top_level_and_environment def _initialize_run_options(self, detach: bool, docker_project_config: Dict[str, Any], debugging_method: Optional[DebuggingMethod]):