Skip to content

Commit 7c888ca

Browse files
Merge pull request #353 from QuantConnect/bug-352-duplicate-project-name-handling
Lean cloud pull improvements
2 parents 015c523 + 732f397 commit 7c888ca

File tree

4 files changed

+46
-15
lines changed

4 files changed

+46
-15
lines changed

lean/components/cloud/pull_manager.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from lean.models.api import QCProject, QCLanguage, QCProjectLibrary
2323
from lean.models.errors import RequestFailedError
2424
from lean.models.utils import LeanLibraryReference
25+
from lean.components.config.storage import safe_save
26+
2527

2628
class PullManager:
2729
"""The PullManager class is responsible for synchronizing cloud projects to the local drive."""
@@ -153,7 +155,8 @@ def _pull_project(self, project: QCProject) -> Path:
153155
:param project: the cloud project to pull
154156
:return the actual local path of the project
155157
"""
156-
local_project_path = self._project_manager.get_local_project_path(project.name, project.projectId)
158+
local_project_path = self._project_manager.get_local_project_path(project.name, project.projectId,
159+
allow_corrupted=True)
157160
local_project_name = local_project_path.relative_to(Path.cwd()).as_posix()
158161
# Check if cloud project has invalid name, if so update it and inform user.
159162
if local_project_name != project.name:
@@ -215,14 +218,12 @@ def _pull_files(self, project: QCProject, local_project_path: Path) -> None:
215218

216219
local_file_path.parent.mkdir(parents=True, exist_ok=True)
217220

218-
with local_file_path.open("w+", encoding="utf-8") as local_file:
219-
if cloud_file.content != "":
220-
# Make sure we always work with unix line endings in memory,
221-
# so they can be properly translated to the local OS line endings when writing to disk.
222-
content = cloud_file.content.replace("\r\n", "\n")
223-
if not content.endswith("\n"):
224-
content += "\n"
225-
local_file.write(content)
221+
# Make sure we always work with unix line endings in memory,
222+
# so they can be properly translated to the local OS line endings when writing to disk.
223+
content = cloud_file.content.replace("\r\n", "\n")
224+
if content != "" and not content.endswith("\n"):
225+
content += "\n"
226+
safe_save(content, local_file_path)
226227

227228
self._project_manager.update_last_modified_time(local_file_path, cloud_file.modified)
228229
self._logger.info(f"Successfully pulled '{project.name}/{cloud_file.name}'")

lean/components/config/storage.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ def __init__(self, file: str) -> None:
7575
else:
7676
self._data = {}
7777

78+
def is_empty(self) -> bool:
79+
"""Determines if this storage file is empty
80+
81+
:return: True if this storage file is empty
82+
"""
83+
return len(self._data) == 0
84+
7885
def get(self, key: str, default: Any = None) -> Any:
7986
"""Returns the value assigned to the given key.
8087

lean/components/util/project_manager.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,16 +203,18 @@ def delete_project(self, project_dir: Path) -> None:
203203
raise RuntimeError(f"Failed to delete project. Could not find the specified path {project_dir}.")
204204

205205

206-
def get_local_project_path(self, project_name: str, cloud_id: Optional[int] = None, local_id: Optional[int] = None) -> Path:
206+
def get_local_project_path(self, project_name: str, cloud_id: Optional[int] = None, local_id: Optional[int] = None,
207+
allow_corrupted: Optional[bool] = False) -> Path:
207208
"""Returns the local path where a certain cloud project should be stored.
208209
209210
If two cloud projects are named "Project", they are pulled to ./Project and ./Project 2.
210211
211212
If you push a project with unsupported cloud name, a supported project name would be assigned.
212213
213-
:param project_name: the cloud project to get the project path of
214-
:param cloud_id: the cloud project to get the project path of
215-
:param local_id: the cloud project to get the project path of
214+
:param project_name: the cloud project name to get the project path of
215+
:param cloud_id: the cloud project id to get the project path of
216+
:param local_id: the cloud project local id to get the project path of
217+
:param allow_corrupted: true if a corrupted path can be used
216218
:return: the path to the local project directory
217219
"""
218220

@@ -226,6 +228,7 @@ def get_local_project_path(self, project_name: str, cloud_id: Optional[int] = No
226228

227229
current_index = 1
228230
while True:
231+
# we first check the current project name
229232
path_suffix = "" if current_index == 1 else f" {current_index}"
230233
current_path = Path.cwd() / (local_path + path_suffix)
231234

@@ -234,14 +237,33 @@ def get_local_project_path(self, project_name: str, cloud_id: Optional[int] = No
234237

235238
if cloud_id is not None:
236239
current_project_config = self._project_config_manager.get_project_config(current_path)
237-
if current_project_config.get("cloud-id") == cloud_id:
240+
if current_project_config.is_empty():
241+
self._logger.error(f"'{current_path}' '{PROJECT_CONFIG_FILE_NAME}' file is corrupted!")
242+
if allow_corrupted:
243+
return current_path
244+
elif current_project_config.get("cloud-id") == cloud_id:
238245
return current_path
239246

240247
if local_id is not None:
241248
current_project_config = self._project_config_manager.get_project_config(current_path)
242-
if current_project_config.get("local-id") == local_id:
249+
if current_project_config.is_empty():
250+
self._logger.error(f"'{current_path}' '{PROJECT_CONFIG_FILE_NAME}' file is corrupted!")
251+
if allow_corrupted:
252+
return current_path
253+
elif current_project_config.get("local-id") == local_id:
243254
return current_path
244255

256+
if current_index == 1:
257+
from re import findall
258+
259+
ints_in_name = findall(r"(\s\d+)$", local_path)
260+
if len(ints_in_name) != 0:
261+
# the current project name already exists, and it has a 'space + int' at the end of it
262+
# so, we take that int and increment it
263+
int_value = ints_in_name[0]
264+
# we remove the int from the name because we will re add it in the loop
265+
local_path = local_path[:-len(int_value)]
266+
current_index = int(int_value)
245267
current_index += 1
246268

247269
def rename_project_and_contents(self, old_path: Path, new_path: Path,) -> None:

tests/components/cloud/test_cloud_project_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def test_get_cloud_project_pushing_new_project():
3636

3737
project_config = mock.Mock()
3838
project_config.get = mock.MagicMock(return_value=cloud_project.projectId)
39+
project_config.is_empty = mock.MagicMock(return_value=False)
3940
project_config_manager = mock.Mock()
4041
project_config_manager.try_get_project_config = mock.MagicMock(return_value=None)
4142
project_config_manager.get_project_config = mock.MagicMock(return_value=project_config)

0 commit comments

Comments
 (0)