Skip to content

Commit 6cc7531

Browse files
authored
Start sending codeSourceId with Cloud Push (#593)
* Initial solution * Add unit tests * Add missing force parameter in recursive _push_project call * Add --force suggestion * Move --force hint to the general try-except
1 parent 3c95abf commit 6cc7531

File tree

4 files changed

+77
-21
lines changed

4 files changed

+77
-21
lines changed

lean/commands/cloud/push.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
@option("--key",
3535
type=PathParameter(exists=True, file_okay=True, dir_okay=False),
3636
help="Path to the encryption key to use")
37-
def push(project: Optional[Path], encrypt: Optional[bool], decrypt: Optional[bool], key: Optional[Path]) -> None:
37+
@option("--force",
38+
is_flag=True, default=False,
39+
help="Force push even if there's a lock conflict")
40+
def push(project: Optional[Path], encrypt: Optional[bool], decrypt: Optional[bool], key: Optional[Path], force: Optional[bool]) -> None:
3841
"""Push local projects to QuantConnect.
3942
4043
This command overrides the content of cloud files with the content of their respective local counterparts.
@@ -61,11 +64,11 @@ def push(project: Optional[Path], encrypt: Optional[bool], decrypt: Optional[boo
6164

6265
if encrypt and key is not None:
6366
from lean.components.util.encryption_helper import validate_encryption_key_registered_with_cloud
64-
validate_encryption_key_registered_with_cloud(key, container.organization_manager, container.api_client)
67+
validate_encryption_key_registered_with_cloud(key, container.organization_manager, container.api_client)
6568

66-
push_manager.push_project(project, encryption_action, key)
69+
push_manager.push_project(project, encryption_action, key, force)
6770
else:
6871
if key is not None:
6972
raise RuntimeError(f"Encryption key can only be specified when pushing a single project.")
7073
projects_to_push = [p.parent for p in Path.cwd().rglob(PROJECT_CONFIG_FILE_NAME)]
71-
push_manager.push_projects(projects_to_push, [], encryption_action, key)
74+
push_manager.push_projects(projects_to_push, [], encryption_action, key, force)

lean/components/api/project_client.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ def update(self,
8383
python_venv: Optional[int] = None,
8484
files: Optional[List[Dict[str, str]]] = None,
8585
libraries: Optional[List[int]] = None,
86-
encryption_key: Optional[str] = None) -> None:
86+
encryption_key: Optional[str] = None,
87+
code_source_id: Optional[str] = None) -> None:
8788
"""Updates an existing project.
8889
8990
:param project_id: the id of the project to update
@@ -94,6 +95,7 @@ def update(self,
9495
:param python_venv: the python venv id for the project, or None if the python venv shouldn't be changed
9596
:param files: the list of files for the project
9697
:param libraries: the list of libraries referenced by the project
98+
:param code_source_id: the source of the code changes (e.g., "cli")
9799
"""
98100
request_parameters = {
99101
"projectId": project_id
@@ -136,7 +138,10 @@ def update(self,
136138

137139
if encryption_key is not None:
138140
request_parameters["encryptionKey"] = encryption_key
139-
141+
142+
if code_source_id is not None:
143+
request_parameters["codeSourceId"] = code_source_id
144+
140145
self._api.post("projects/update", request_parameters, data_as_json=False)
141146

142147
def delete(self, project_id: int) -> None:

lean/components/cloud/push_manager.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,17 @@ def __init__(self,
4646
self._organization_manager = organization_manager
4747
self._cloud_projects = []
4848

49-
def push_project(self, project: Path, encryption_action: Optional[ActionType]=None, encryption_key: Optional[Path]=None) -> None:
49+
def push_project(self, project: Path, encryption_action: Optional[ActionType]=None, encryption_key: Optional[Path]=None, force: Optional[bool]=False) -> None:
5050
"""Pushes the given project from the local drive to the cloud.
5151
5252
It will also push every library referenced by the project and add or remove references.
5353
5454
:param project: path to the directory containing the local project that needs to be pushed
5555
"""
5656
libraries = self._project_manager.get_project_libraries(project)
57-
self.push_projects([project], libraries, encryption_action, encryption_key)
57+
self.push_projects([project], libraries, encryption_action, encryption_key, force)
5858

59-
def push_projects(self, projects_to_push: List[Path], associated_libraries_to_push: Optional[List[Path]]=[], encryption_action: Optional[ActionType]=None, encryption_key: Optional[Path]=None) -> None:
59+
def push_projects(self, projects_to_push: List[Path], associated_libraries_to_push: Optional[List[Path]]=[], encryption_action: Optional[ActionType]=None, encryption_key: Optional[Path]=None, force: Optional[bool]=False) -> None:
6060
"""Pushes the given projects from the local drive to the cloud.
6161
6262
It will also push every library referenced by each project and add or remove references.
@@ -78,11 +78,13 @@ def push_projects(self, projects_to_push: List[Path], associated_libraries_to_pu
7878
relative_path = path.relative_to(Path.cwd())
7979
try:
8080
self._logger.info(f"[{index}/{len(all_projects_to_push)}] Pushing '{relative_path}'")
81-
self._push_project(path, organization_id, encryption_action_value, encryption_key_value)
81+
self._push_project(path, organization_id, encryption_action_value, encryption_key_value, force=force)
8282
except Exception as ex:
8383
from traceback import format_exc
8484
self._logger.debug(format_exc().strip())
8585
self._logger.warn(f"Cannot push '{relative_path}': {ex}")
86+
if "write permission" in str(ex).lower():
87+
self._logger.info("Please pull any required changes and push with --force")
8688

8789
def _get_local_libraries_cloud_ids(self, project_dir: Path) -> List[int]:
8890
project_config = self._project_config_manager.get_project_config(project_dir)
@@ -95,7 +97,7 @@ def _get_local_libraries_cloud_ids(self, project_dir: Path) -> List[int]:
9597

9698
return local_libraries_cloud_ids
9799

98-
def _push_project(self, project_path: Path, organization_id: str, encryption_action: Optional[ActionType], encryption_key: Optional[Path], suggested_rename_path: Path = None) -> None:
100+
def _push_project(self, project_path: Path, organization_id: str, encryption_action: Optional[ActionType], encryption_key: Optional[Path], force: Optional[bool], suggested_rename_path: Path = None) -> None:
99101
"""Pushes a single local project to the cloud.
100102
101103
Raises an error with a descriptive message if the project cannot be pushed.
@@ -111,7 +113,6 @@ def _push_project(self, project_path: Path, organization_id: str, encryption_act
111113
if suggested_rename_path and suggested_rename_path != project_path:
112114
potential_new_name = suggested_rename_path.relative_to(Path.cwd()).as_posix()
113115

114-
115116
project_config = self._project_config_manager.get_project_config(project_path)
116117
cloud_id = project_config.get("cloud-id")
117118
local_encryption_state = project_config.get("encrypted", False)
@@ -149,7 +150,7 @@ def _push_project(self, project_path: Path, organization_id: str, encryption_act
149150
if cloud_project.name != project_name:
150151
# cloud project name was changed. Repeat steps to validate the new name locally.
151152
self._logger.info(f"Received new name '{cloud_project.name}' for project '{project_name}' from QuantConnect.com")
152-
self._push_project(project_path, organization_id, encryption_action, encryption_key, Path.cwd() / cloud_project.name)
153+
self._push_project(project_path, organization_id, encryption_action, encryption_key, force, Path.cwd() / cloud_project.name)
153154
return
154155

155156
self._cloud_projects.append(cloud_project)
@@ -163,7 +164,7 @@ def _push_project(self, project_path: Path, organization_id: str, encryption_act
163164
encryption_key = local_encryption_key
164165
encryption_action = ActionType.ENCRYPT if local_encryption_state else ActionType.DECRYPT
165166
# Finalize pushing by updating locally modified metadata, files and libraries
166-
self._push_metadata(project_path, cloud_project, encryption_action, encryption_key)
167+
self._push_metadata(project_path, cloud_project, encryption_action, encryption_key, force)
167168

168169
def _get_files(self, project: Path, encryption_action: Optional[ActionType], encryption_key: Optional[Path]) -> List[Dict[str, str]]:
169170
"""Pushes the files of a local project to the cloud.
@@ -193,7 +194,7 @@ def _get_files(self, project: Path, encryption_action: Optional[ActionType], enc
193194

194195
return files
195196

196-
def _push_metadata(self, project: Path, cloud_project: QCProject, encryption_action: Optional[ActionType], encryption_key: Optional[Path]) -> None:
197+
def _push_metadata(self, project: Path, cloud_project: QCProject, encryption_action: Optional[ActionType], encryption_key: Optional[Path], force: Optional[bool]) -> None:
197198
"""Pushes local project description and parameters to the cloud.
198199
199200
Does nothing if the cloud is already up-to-date.
@@ -255,18 +256,22 @@ def _push_metadata(self, project: Path, cloud_project: QCProject, encryption_act
255256
encryption_key_id = get_project_key_hash(encryption_key)
256257
update_args["encryption_key"] = encryption_key_id
257258

259+
if not force:
260+
update_args["code_source_id"] = "cli"
258261
if update_args != {}:
259262
self._api_client.projects.update(cloud_project.projectId, **update_args)
260263

261264
if "encryption_key" in update_args:
262265
del update_args["encryption_key"]
266+
263267
updated_keys = list(update_args)
264268
if len(updated_keys) == 1:
265269
updated_keys_str = updated_keys[0]
266270
elif len(updated_keys) == 2:
267271
updated_keys_str = " and ".join(updated_keys)
268272
else:
269273
updated_keys_str = ", ".join(updated_keys[:-1]) + f", and {updated_keys[-1]}"
274+
270275
self._logger.info(f"Successfully updated {updated_keys_str} for '{cloud_project.name}'")
271276

272277
def _get_cloud_project(self, project_id: int, organization_id: str) -> QCProject:

0 commit comments

Comments
 (0)