Skip to content

Commit c502bd2

Browse files
Merge pull request #206 from jhonabreul/push-command-performance-improvements
Push command performance improvements
2 parents 91fd51b + 3ea97a9 commit c502bd2

File tree

5 files changed

+87
-105
lines changed

5 files changed

+87
-105
lines changed

lean/components/api/project_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from typing import List, Optional
1515

1616
from lean.components.api.api_client import *
17-
from lean.models.api import QCCreatedProject, QCLanguage, QCProject
17+
from lean.models.api import QCLanguage, QCProject
1818

1919

2020
class ProjectClient:
@@ -47,7 +47,7 @@ def get_all(self) -> List[QCProject]:
4747
data = self._api.get("projects/read")
4848
return [self._process_project(QCProject(**project)) for project in data["projects"]]
4949

50-
def create(self, name: str, language: QCLanguage, organization_id: Optional[str]) -> QCCreatedProject:
50+
def create(self, name: str, language: QCLanguage, organization_id: Optional[str]) -> QCProject:
5151
"""Creates a new project.
5252
5353
:param name: the name of the project to create
@@ -63,7 +63,7 @@ def create(self, name: str, language: QCLanguage, organization_id: Optional[str]
6363
parameters["organizationId"] = organization_id
6464
data = self._api.post("projects/create", parameters)
6565

66-
return self._process_project(QCCreatedProject(**data["projects"][0]))
66+
return self._process_project(QCProject(**data["projects"][0]))
6767

6868
def update(self,
6969
project_id: int,

lean/components/cloud/push_manager.py

Lines changed: 47 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from lean.components.config.project_config_manager import ProjectConfigManager
2020
from lean.components.util.logger import Logger
2121
from lean.components.util.project_manager import ProjectManager
22-
from lean.models.api import QCLanguage, QCProject, QCLeanEnvironment
22+
from lean.models.api import QCLanguage, QCProject
2323
from lean.models.utils import LeanLibraryReference
2424

2525

@@ -43,6 +43,7 @@ def __init__(self,
4343
self._project_manager = project_manager
4444
self._project_config_manager = project_config_manager
4545
self._last_file = None
46+
self._cloud_projects = []
4647

4748
def push_projects(self, projects_to_push: List[Path], organization_id: Optional[str] = None) -> None:
4849
"""Pushes the given projects from the local drive to the cloud.
@@ -52,42 +53,34 @@ def push_projects(self, projects_to_push: List[Path], organization_id: Optional[
5253
:param projects_to_push: a list of directories containing the local projects that need to be pushed
5354
:param organization_id: the id of the organization where the project will be pushed to
5455
"""
55-
projects = projects_to_push + [library
56-
for project in projects_to_push
57-
for library in self._project_manager.get_project_libraries(project)]
58-
projects = sorted(projects)
56+
if len(projects_to_push) == 0:
57+
return
5958

60-
cloud_projects = self._api_client.projects.get_all()
61-
environments = self._api_client.lean.environments()
59+
projects_paths = projects_to_push + [library
60+
for project in projects_to_push
61+
for library in self._project_manager.get_project_libraries(project)]
62+
projects_paths = sorted(projects_paths)
6263

6364
pushed_projects = {}
64-
65-
for index, project in enumerate(projects, start=1):
66-
relative_path = project.relative_to(Path.cwd())
65+
for index, path in enumerate(projects_paths, start=1):
66+
relative_path = path.relative_to(Path.cwd())
6767
try:
68-
self._logger.info(f"[{index}/{len(projects)}] Pushing '{relative_path}'")
69-
pushed_project = self._push_project(project, cloud_projects, organization_id, environments)
70-
pushed_projects[project] = pushed_project
68+
self._logger.info(f"[{index}/{len(projects_paths)}] Pushing '{relative_path}'")
69+
pushed_projects[path] = self._push_project(path, organization_id)
7170
except Exception as ex:
7271
self._logger.debug(traceback.format_exc().strip())
7372
if self._last_file is not None:
7473
self._logger.warn(f"Cannot push '{relative_path}' (failed on {self._last_file}): {ex}")
7574
else:
7675
self._logger.warn(f"Cannot push '{relative_path}': {ex}")
7776

78-
pushed_cloud_projects = pushed_projects.values()
79-
cloud_projects = [project for project in cloud_projects if project.projectId not in pushed_cloud_projects]
80-
cloud_projects.extend(pushed_cloud_projects)
81-
82-
self._update_cloud_library_references(pushed_projects, cloud_projects)
77+
self._update_cloud_library_references(pushed_projects)
8378

84-
def _update_cloud_library_references(self, projects: Dict[Path, QCProject],
85-
cloud_projects: List[QCProject]) -> None:
79+
def _update_cloud_library_references(self, projects: Dict[Path, QCProject]) -> None:
8680
for path, project in projects.items():
8781
local_libraries_cloud_ids = self._get_local_libraries_cloud_ids(path)
88-
89-
self._add_new_libraries(project, local_libraries_cloud_ids, cloud_projects)
90-
self._remove_outdated_libraries(project, local_libraries_cloud_ids, cloud_projects)
82+
self._add_new_libraries(project, local_libraries_cloud_ids)
83+
self._remove_outdated_libraries(project, local_libraries_cloud_ids)
9184

9285
def _get_local_libraries_cloud_ids(self, project_dir: Path) -> List[int]:
9386
project_config = self._project_config_manager.get_project_config(project_dir)
@@ -100,89 +93,69 @@ def _get_local_libraries_cloud_ids(self, project_dir: Path) -> List[int]:
10093

10194
return local_libraries_cloud_ids
10295

103-
@staticmethod
104-
def _get_library_name(library_cloud_id: int, cloud_projects: List[QCProject]) -> str:
105-
return [project.name for project in cloud_projects if project.projectId == library_cloud_id][0]
96+
def _get_library_name(self, library_cloud_id: int) -> str:
97+
return self._get_cloud_project(library_cloud_id).name
10698

107-
def _add_new_libraries(self,
108-
project: QCProject,
109-
local_libraries_cloud_ids: List[int],
110-
cloud_projects: List[QCProject]) -> None:
99+
def _add_new_libraries(self, project: QCProject, local_libraries_cloud_ids: List[int]) -> None:
111100
libraries_to_add = [library_id for library_id in local_libraries_cloud_ids if
112101
library_id not in project.libraries]
113102

114103
if len(libraries_to_add) > 0:
115104
self._logger.info(f"Adding libraries to project {project.name} in the cloud")
116105

117106
for i, library_cloud_id in enumerate(libraries_to_add, start=1):
118-
library_name = self._get_library_name(library_cloud_id, cloud_projects)
107+
library_name = self._get_library_name(library_cloud_id)
119108
self._logger.info(f"[{i}/{len(libraries_to_add)}] "
120109
f"Adding library {library_name} to project {project.name} in the cloud")
121110
self._api_client.projects.add_library(project.projectId, library_cloud_id)
122111

123-
def _remove_outdated_libraries(self,
124-
project: QCProject,
125-
local_libraries_cloud_ids: List[int],
126-
cloud_projects: List[QCProject]) -> None:
112+
def _remove_outdated_libraries(self, project: QCProject, local_libraries_cloud_ids: List[int]) -> None:
127113
libraries_to_remove = [library_id for library_id in project.libraries
128114
if library_id not in local_libraries_cloud_ids]
129115

130116
if len(libraries_to_remove) > 0:
131117
self._logger.info(f"Removing libraries from project {project.name} in the cloud")
132118

133119
for i, library_cloud_id in enumerate(libraries_to_remove, start=1):
134-
library_name = self._get_library_name(library_cloud_id, cloud_projects)
120+
library_name = self._get_library_name(library_cloud_id)
135121
self._logger.info(f"[{i}/{len(libraries_to_remove)}] "
136122
f"Removing library {library_name} from project {project.name} in the cloud")
137123
self._api_client.projects.delete_library(project.projectId, library_cloud_id)
138124

139-
def _push_project(self,
140-
project: Path,
141-
cloud_projects: List[QCProject],
142-
organization_id: Optional[str],
143-
environments: List[QCLeanEnvironment]) -> QCProject:
125+
def _push_project(self, project: Path, organization_id: Optional[str]) -> QCProject:
144126
"""Pushes a single local project to the cloud.
145127
146128
Raises an error with a descriptive message if the project cannot be pushed.
147129
148130
:param project: the local project to push
149-
:param cloud_projects: a list containing all of the user's cloud projects
150131
:param organization_id: the id of the organization to push the project to
151-
:param environments: list of available lean environments
152132
"""
153133
project_name = project.relative_to(Path.cwd()).as_posix()
154134

155135
project_config = self._project_config_manager.get_project_config(project)
156136
cloud_id = project_config.get("cloud-id")
157137

158-
cloud_project_by_id = next(iter([p for p in cloud_projects if p.projectId == cloud_id]), None)
159-
160138
# Find the cloud project to push the files to
161-
if cloud_project_by_id is not None:
139+
if cloud_id is not None:
162140
# Project has cloud id which matches cloud project, update cloud project
163-
cloud_project = cloud_project_by_id
141+
cloud_project = self._get_cloud_project(cloud_id)
164142
else:
165143
# Project has invalid cloud id or no cloud id at all, create new cloud project
166-
new_project = self._api_client.projects.create(project_name,
167-
QCLanguage[project_config.get("algorithm-language")],
168-
organization_id)
144+
cloud_project = self._api_client.projects.create(project_name,
145+
QCLanguage[project_config.get("algorithm-language")],
146+
organization_id)
147+
self._cloud_projects.append(cloud_project)
148+
project_config.set("cloud-id", cloud_project.projectId)
149+
project_config.set("organization-id", cloud_project.organizationId)
169150

170151
organization_message_part = f" in organization '{organization_id}'" if organization_id is not None else ""
171152
self._logger.info(f"Successfully created cloud project '{project_name}'{organization_message_part}")
172153

173-
project_config.set("cloud-id", new_project.projectId)
174-
175-
# We need to retrieve the created project again to get all project details
176-
cloud_project = self._api_client.projects.get(new_project.projectId)
177-
178-
# set organization-id in project config
179-
project_config.set("organization-id", cloud_project.organizationId)
180-
181154
# Push local files to cloud
182155
self._push_files(project, cloud_project)
183156

184157
# Finalize pushing by updating locally modified metadata
185-
self._push_metadata(project, cloud_project, environments)
158+
self._push_metadata(project, cloud_project)
186159

187160
return cloud_project
188161

@@ -225,7 +198,7 @@ def _push_files(self, project: Path, cloud_project: QCProject) -> None:
225198

226199
self._last_file = None
227200

228-
def _push_metadata(self, project: Path, cloud_project: QCProject, environments: List[QCLeanEnvironment]) -> None:
201+
def _push_metadata(self, project: Path, cloud_project: QCProject) -> None:
229202
"""Pushes local project description and parameters to the cloud.
230203
231204
Does nothing if the cloud is already up-to-date.
@@ -245,8 +218,7 @@ def _push_metadata(self, project: Path, cloud_project: QCProject, environments:
245218
local_lean_version = int(project_config.get("lean-engine", "-1"))
246219
cloud_lean_version = cloud_project.leanVersionId
247220

248-
default_lean_venv = next((env.id for env in environments if env.path is None), None)
249-
local_lean_venv = project_config.get("python-venv", default_lean_venv)
221+
local_lean_venv = project_config.get("python-venv", None)
250222
cloud_lean_venv = cloud_project.leanEnvironment
251223

252224
update_args = {}
@@ -258,12 +230,23 @@ def _push_metadata(self, project: Path, cloud_project: QCProject, environments:
258230
update_args["parameters"] = local_parameters
259231

260232
if (local_lean_version != cloud_lean_version and
261-
(local_lean_version != -1 or not cloud_project.leanPinnedToMaster)):
233+
(local_lean_version != -1 or not cloud_project.leanPinnedToMaster)):
262234
update_args["lean_engine"] = local_lean_version
263235

264-
if local_lean_venv != cloud_lean_venv:
236+
# Initially, python-venv is not defined in the config and the default one will be used.
237+
# After it is changed, in order to use the default one again, it must not be removed from the config,
238+
# but it should be set to the default env id explicitly instead.
239+
if local_lean_venv is not None and local_lean_venv != cloud_lean_venv:
265240
update_args["python_venv"] = local_lean_venv
266241

267242
if update_args != {}:
268243
self._api_client.projects.update(cloud_project.projectId, **update_args)
269244
self._logger.info(f"Successfully updated {' and '.join(update_args.keys())} for '{cloud_project.name}'")
245+
246+
def _get_cloud_project(self, project_id: int) -> QCProject:
247+
project = next(iter(p for p in self._cloud_projects if p.projectId == project_id), None)
248+
if project is None:
249+
project = self._api_client.projects.get(project_id)
250+
self._cloud_projects.append(project)
251+
252+
return project

tests/commands/cloud/test_push.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,16 @@ def test_cloud_push_removes_locally_removed_files_in_cloud() -> None:
112112
create_fake_lean_cli_directory()
113113

114114
client = mock.Mock()
115-
fake_cloud_files = [QCFullFile(name="removed_file.py", content="", modified=datetime.now(), isLibrary=False)]
115+
fake_cloud_files = [QCFullFile(name="removed_file.py", content="SomeContent", modified=datetime.now(), isLibrary=False)]
116116
client.files.get_all = mock.MagicMock(return_value=fake_cloud_files)
117117
client.files.delete = mock.Mock()
118118
client.lean.environments = mock.MagicMock(return_value=create_lean_environments())
119119

120-
cloud_projects = [create_api_project(1, "Python Project")]
121-
client.projects.get_all = mock.MagicMock(return_value=cloud_projects)
120+
cloud_project = create_api_project(1, "Python Project")
121+
client.projects.get = mock.MagicMock(return_value=cloud_project)
122122

123123
project_config = mock.Mock()
124-
project_config.get = mock.MagicMock(side_effect=[1, "", {}, cloud_projects[0].leanVersionId, None, []])
124+
project_config.get = mock.MagicMock(side_effect=[1, "", {}, cloud_project.leanVersionId, None, []])
125125

126126
project_config_manager = mock.Mock()
127127
project_config_manager.get_project_config = mock.MagicMock(return_value=project_config)
@@ -139,7 +139,7 @@ def test_cloud_push_removes_locally_removed_files_in_cloud() -> None:
139139
assert result.exit_code == 0
140140

141141
project_config.get.assert_called()
142-
client.projects.get_all.assert_called()
142+
client.projects.get.assert_called_once_with(cloud_project.projectId)
143143
project_manager.get_source_files.assert_called_once()
144144
project_config_manager.get_project_config.assert_called()
145145
client.files.get_all.assert_called_once()
@@ -153,17 +153,13 @@ def test_cloud_push_creates_project_with_optional_organization_id(organization_i
153153
path = "Python Project"
154154
cloud_project = create_api_project(1, path)
155155

156-
with mock.patch.object(ProjectClient, 'create', return_value=create_api_project(1, path)) as mock_create_project,\
157-
mock.patch.object(ProjectClient, 'get_all', side_effect=[[], [cloud_project]]) as mock_get_all_projects,\
158-
mock.patch.object(LeanClient, 'environments', return_value=create_lean_environments()) as mock_get_environments:
156+
with mock.patch.object(ProjectClient, 'create', return_value=create_api_project(1, path)) as mock_create_project:
159157
organization_id_option = ["--organization-id", organization_id] if organization_id is not None else []
160158
result = CliRunner().invoke(lean, ["cloud", "push", "--project", path, *organization_id_option])
161159

162160
assert result.exit_code == 0
163161

164-
mock_get_all_projects.assert_called()
165162
mock_create_project.assert_called_once_with(path, QCLanguage.Python, organization_id)
166-
mock_get_environments.assert_called_once()
167163

168164

169165
def test_cloud_push_updates_lean_config() -> None:

tests/components/cloud/test_cloud_project_manager.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,10 @@
2424
def test_get_cloud_project_pushing_new_project():
2525
create_fake_lean_cli_directory()
2626

27-
cloud_projects = [create_api_project(i, f"Project {i}") for i in range(1, 11)]
2827
cloud_project = create_api_project(20, "Python Project")
2928
cloud_project.description = ""
3029

3130
api_client = mock.Mock()
32-
api_client.projects.get_all = mock.MagicMock(side_effect=[cloud_projects, [*cloud_projects, cloud_project]])
3331
api_client.projects.get.return_value = cloud_project
3432
api_client.projects.create.return_value = cloud_project
3533
api_client.files.get_all.return_value = []
@@ -42,5 +40,4 @@ def test_get_cloud_project_pushing_new_project():
4240

4341
assert created_cloud_project == cloud_project
4442

45-
api_client.projects.get_all.assert_called_once()
4643
api_client.projects.get.assert_called_with(cloud_project.projectId)

0 commit comments

Comments
 (0)