Skip to content

Commit ca659d2

Browse files
committed
Initial solution
1 parent aec6fcc commit ca659d2

File tree

4 files changed

+51
-15
lines changed

4 files changed

+51
-15
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] = "cli") -> 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: 10 additions & 8 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,7 +78,7 @@ 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())
@@ -95,7 +95,7 @@ def _get_local_libraries_cloud_ids(self, project_dir: Path) -> List[int]:
9595

9696
return local_libraries_cloud_ids
9797

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:
98+
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:
9999
"""Pushes a single local project to the cloud.
100100
101101
Raises an error with a descriptive message if the project cannot be pushed.
@@ -111,7 +111,6 @@ def _push_project(self, project_path: Path, organization_id: str, encryption_act
111111
if suggested_rename_path and suggested_rename_path != project_path:
112112
potential_new_name = suggested_rename_path.relative_to(Path.cwd()).as_posix()
113113

114-
115114
project_config = self._project_config_manager.get_project_config(project_path)
116115
cloud_id = project_config.get("cloud-id")
117116
local_encryption_state = project_config.get("encrypted", False)
@@ -163,7 +162,7 @@ def _push_project(self, project_path: Path, organization_id: str, encryption_act
163162
encryption_key = local_encryption_key
164163
encryption_action = ActionType.ENCRYPT if local_encryption_state else ActionType.DECRYPT
165164
# Finalize pushing by updating locally modified metadata, files and libraries
166-
self._push_metadata(project_path, cloud_project, encryption_action, encryption_key)
165+
self._push_metadata(project_path, cloud_project, encryption_action, encryption_key, force)
167166

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

194193
return files
195194

196-
def _push_metadata(self, project: Path, cloud_project: QCProject, encryption_action: Optional[ActionType], encryption_key: Optional[Path]) -> None:
195+
def _push_metadata(self, project: Path, cloud_project: QCProject, encryption_action: Optional[ActionType], encryption_key: Optional[Path], force: Optional[bool]) -> None:
197196
"""Pushes local project description and parameters to the cloud.
198197
199198
Does nothing if the cloud is already up-to-date.
@@ -260,13 +259,16 @@ def _push_metadata(self, project: Path, cloud_project: QCProject, encryption_act
260259

261260
if "encryption_key" in update_args:
262261
del update_args["encryption_key"]
262+
if not force:
263+
update_args["code_source_id"] = "cli"
263264
updated_keys = list(update_args)
264265
if len(updated_keys) == 1:
265266
updated_keys_str = updated_keys[0]
266267
elif len(updated_keys) == 2:
267268
updated_keys_str = " and ".join(updated_keys)
268269
else:
269270
updated_keys_str = ", ".join(updated_keys[:-1]) + f", and {updated_keys[-1]}"
271+
270272
self._logger.info(f"Successfully updated {updated_keys_str} for '{cloud_project.name}'")
271273

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

tests/commands/cloud/test_push.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def test_cloud_push_pushes_single_project_when_project_option_given() -> None:
8282

8383
assert result.exit_code == 0
8484

85-
push_manager.push_project.assert_called_once_with(Path.cwd() / "Python Project", None, None)
85+
push_manager.push_project.assert_called_once_with(Path.cwd() / "Python Project", None, None, False)
8686

8787

8888
def test_cloud_push_aborts_when_given_directory_is_not_lean_project() -> None:
@@ -469,3 +469,29 @@ def _get_expected_encrypted_files_content() -> dict:
469469
UGw0ehtO8qY5FmPGcUlkBGuqmd7r6aLE4mosoZrc/UyZb+clWNYJITRLFJbQpWm3EU/Xrt5UM8uWwEdV
470470
bFWAAkX56MyDHwJefC1nkA=="""
471471
}
472+
473+
def test_cloud_push_sets_code_source_id_to_cli() -> None:
474+
create_fake_lean_cli_directory()
475+
project_path = Path.cwd() / "Python Project"
476+
477+
api_client = mock.Mock()
478+
cloud_project = create_api_project(1, "Python Project")
479+
api_client.projects.create = mock.MagicMock(return_value=cloud_project)
480+
api_client.files.get_all = mock.MagicMock(return_value=[
481+
QCFullFile(name="file.py", content="print(123)", modified=datetime.now(), isLibrary=False)
482+
])
483+
484+
init_container(api_client_to_use=api_client)
485+
486+
result = CliRunner().invoke(lean, ["cloud", "push", "--project", project_path])
487+
488+
assert result.exit_code == 0
489+
expected_arguments = {
490+
"name": "Python Project",
491+
"description": "",
492+
"files": mock.ANY,
493+
"libraries": [],
494+
"encryption_key": "",
495+
"codeSourceId": "cli"
496+
}
497+
api_client.projects.update.assert_called_once_with(1, **expected_arguments)

0 commit comments

Comments
 (0)