Skip to content

Commit 91fd51b

Browse files
Merge pull request #207 from jhonabreul/pull-command-performance-improvements
Pull command performance improvements
2 parents 9961534 + 630c898 commit 91fd51b

File tree

4 files changed

+56
-52
lines changed

4 files changed

+56
-52
lines changed

lean/commands/cloud/pull.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,24 @@ def pull(project: Optional[str], pull_bootcamp: bool) -> None:
3131
"""
3232
# Parse which projects need to be pulled
3333
project_id = None
34+
project_name = None
3435
if project is not None:
3536
try:
3637
project_id = int(project)
3738
except ValueError:
38-
pass
39+
# We treat it as a name rather than an id
40+
project_name = project
3941

4042
api_client = container.api_client()
41-
all_projects = api_client.projects.get_all()
42-
project_manager = container.project_manager()
43-
projects_to_pull = project_manager.get_projects_by_name_or_id(all_projects, project_id or project)
43+
projects_to_pull = []
44+
all_projects = None
45+
46+
if project_id is not None:
47+
projects_to_pull.append(api_client.projects.get(project_id))
48+
else:
49+
all_projects = api_client.projects.get_all()
50+
project_manager = container.project_manager()
51+
projects_to_pull = project_manager.get_projects_by_name_or_id(all_projects, project_name)
4452

4553
if project is None and not pull_bootcamp:
4654
projects_to_pull = [p for p in projects_to_pull if not p.name.startswith("Boot Camp/")]

lean/components/cloud/pull_manager.py

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313

1414
import traceback
1515
from pathlib import Path
16-
from typing import List, Dict
16+
from typing import List, Optional, Tuple
1717

1818
from lean.components.api.api_client import APIClient
1919
from lean.components.config.project_config_manager import ProjectConfigManager
2020
from lean.components.util.library_manager import LibraryManager
2121
from lean.components.util.logger import Logger
2222
from lean.components.util.platform_manager import PlatformManager
2323
from lean.components.util.project_manager import ProjectManager
24-
from lean.models.api import QCProject, QCLeanEnvironment, QCLanguage
24+
from lean.models.api import QCProject, QCLanguage
2525
from lean.models.utils import LeanLibraryReference
2626

2727

@@ -52,38 +52,54 @@ def __init__(self,
5252
self._platform_manager = platform_manager
5353
self._last_file = None
5454

55-
def pull_projects(self, projects_to_pull: List[QCProject], all_cloud_projects: List[QCProject]) -> None:
55+
def _get_libraries(self, project: QCProject, seen_projects: List[int] = None) -> List[QCProject]:
56+
if seen_projects is None:
57+
seen_projects = [project.projectId]
58+
59+
libraries = []
60+
for library_id in project.libraries:
61+
if library_id in seen_projects:
62+
continue
63+
seen_projects.append(library_id)
64+
library = self._api_client.projects.get(library_id)
65+
libraries.append(library)
66+
libraries.extend(self._get_libraries(library, seen_projects))
67+
68+
return libraries
69+
70+
def pull_projects(self, projects_to_pull: List[QCProject], all_cloud_projects: Optional[List[QCProject]]) -> None:
5671
"""Pulls the given projects from the cloud to the local drive.
5772
5873
This will also pull libraries referenced by the project.
5974
6075
:param projects_to_pull: the cloud projects that need to be pulled
6176
:param all_cloud_projects: all the projects available in the cloud
6277
"""
63-
projects_to_pull.extend(self._project_manager.get_cloud_projects_libraries(all_cloud_projects,
64-
projects_to_pull))
78+
if all_cloud_projects is not None:
79+
projects_to_pull.extend(self._project_manager.get_cloud_projects_libraries(all_cloud_projects,
80+
projects_to_pull))
81+
else:
82+
for project in projects_to_pull:
83+
projects_to_pull.extend(self._get_libraries(project, [p.projectId for p in projects_to_pull]))
84+
6585
projects_to_pull = sorted(projects_to_pull, key=lambda p: p.name)
66-
environments = self._api_client.lean.environments()
67-
projects_not_pulled = []
68-
project_paths = {}
86+
projects_with_paths = []
6987

7088
for index, project in enumerate(projects_to_pull, start=1):
7189
try:
7290
self._logger.info(f"[{index}/{len(projects_to_pull)}] Pulling '{project.name}'")
73-
project_paths[project.projectId] = self._pull_project(project, environments)
91+
projects_with_paths.append((project, self._pull_project(project)))
7492
except Exception as ex:
75-
projects_not_pulled.append(project)
7693
self._logger.debug(traceback.format_exc().strip())
7794
if self._last_file is not None:
7895
self._logger.warn(
7996
f"Cannot pull '{project.name}' (id {project.projectId}, failed on {self._last_file}): {ex}")
8097
else:
8198
self._logger.warn(f"Cannot pull '{project.name}' (id {project.projectId}): {ex}")
8299

83-
projects_to_update = [project for project in projects_to_pull if project not in projects_not_pulled]
84-
self._update_local_library_references(projects_to_update, project_paths)
100+
self._update_local_library_references(projects_with_paths)
85101

86-
def _pull_project(self, project: QCProject, environments: List[QCLeanEnvironment]) -> Path:
102+
def _pull_project(self, project: QCProject) -> Path:
87103
"""Pulls a single project from the cloud to the local drive.
88104
89105
Raises an error with a descriptive message if the project cannot be pulled.
@@ -103,18 +119,13 @@ def _pull_project(self, project: QCProject, environments: List[QCLeanEnvironment
103119
project_config.set("parameters", {parameter.key: parameter.value for parameter in project.parameters})
104120
project_config.set("description", project.description)
105121
project_config.set("organization-id", project.organizationId)
122+
project_config.set("python-venv", project.leanEnvironment)
106123

107124
if not project.leanPinnedToMaster:
108125
project_config.set("lean-engine", project.leanVersionId)
109126
else:
110127
project_config.delete("lean-engine")
111128

112-
python_venv = next((env.path for env in environments if env.id == project.leanEnvironment), None)
113-
if python_venv is not None:
114-
project_config.set("python-venv", project.leanEnvironment)
115-
else:
116-
project_config.delete("python-venv")
117-
118129
return local_project_path
119130

120131
def _pull_files(self, project: QCProject, local_project_path: Path) -> None:
@@ -255,22 +266,19 @@ def _remove_local_library_references_from_project(self,
255266
Path.cwd() / library_reference.path,
256267
True)
257268

258-
def _update_local_library_references(self, projects: List[QCProject], paths: Dict[int, Path]) -> None:
259-
for project in projects:
260-
cloud_libraries_paths = [paths[library_id]
261-
for library_id in project.libraries
262-
for library in projects if library.projectId == library_id]
263-
264-
project_path = paths[project.projectId]
269+
def _update_local_library_references(self, projects: List[Tuple[QCProject, Path]]) -> None:
270+
for project, path in projects:
271+
cloud_libraries_paths = [library_path for library, library_path in projects
272+
if library.projectId in project.libraries]
265273

266274
# Add cloud library references to local config
267-
self._add_local_library_references_to_project(project_path, cloud_libraries_paths)
275+
self._add_local_library_references_to_project(path, cloud_libraries_paths)
268276

269277
# Remove library references locally if they were removed in the cloud
270-
self._remove_local_library_references_from_project(project_path, cloud_libraries_paths)
278+
self._remove_local_library_references_from_project(path, cloud_libraries_paths)
271279

272280
# Restore the project to automatically enable local auto-complete
273-
self._restore_project(project, project_path)
281+
self._restore_project(project, path)
274282

275283
def _restore_project(self, project: QCProject, project_dir: Path) -> None:
276284
if project.language != QCLanguage.CSharp:

tests/commands/cloud/test_pull.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,20 @@ def test_cloud_pull_pulls_project_by_id() -> None:
7575
create_api_project(3, "Project 3"),
7676
create_api_project(4, "Boot Camp/Project 4"),
7777
create_api_project(5, "Boot Camp/Project 5")]
78+
project_to_pull = cloud_projects[0]
7879

7980
api_client = mock.Mock()
80-
api_client.projects.get_all.return_value = cloud_projects
81+
api_client.projects.get.return_value = project_to_pull
8182
container.api_client.override(providers.Object(api_client))
8283

8384
pull_manager = mock.Mock()
8485
container.pull_manager.override(providers.Object(pull_manager))
8586

86-
result = CliRunner().invoke(lean, ["cloud", "pull", "--project", "1"])
87+
result = CliRunner().invoke(lean, ["cloud", "pull", "--project", project_to_pull.projectId])
8788

8889
assert result.exit_code == 0
8990

90-
pull_manager.pull_projects.assert_called_once_with([cloud_projects[0]], cloud_projects)
91+
pull_manager.pull_projects.assert_called_once_with([project_to_pull], None)
9192

9293

9394
def test_cloud_pull_pulls_project_by_name() -> None:

tests/components/util/test_pull_manager.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -113,21 +113,6 @@ def test_pull_manager_removes_lean_engine_from_config_when_lean_pinned_to_master
113113
_assert_pull_manager_removes_property_from_project_config("lean-engine", [cloud_project])
114114

115115

116-
def test_pull_manager_removes_python_venv_from_config_when_set_to_default() -> None:
117-
create_fake_lean_cli_directory()
118-
119-
project_path = Path.cwd() / "Python Project"
120-
config = Storage(str(project_path / "config.json"))
121-
environments = create_lean_environments()
122-
config.set("python-venv", next(env.path for env in environments if env.path is not None))
123-
124-
project_id = 1000
125-
cloud_project = create_api_project(project_id, project_path.name)
126-
cloud_project.leanPinnedToMaster = True
127-
128-
_assert_pull_manager_removes_property_from_project_config("python-venv", [cloud_project])
129-
130-
131116
def _make_cloud_projects_and_libraries(project_count: int,
132117
library_count: int) -> Tuple[List[QCProject], List[QCProject]]:
133118
cloud_projects = [create_api_project(i, f"Project {i}") for i in range(1, project_count + 1)]
@@ -328,6 +313,7 @@ def test_pull_projects_updates_lean_config() -> None:
328313
api_client.files.get_all = mock.MagicMock(return_value=[])
329314

330315
project_config = mock.Mock()
316+
project_config.get = mock.MagicMock(return_value=[]) # get("libraries")
331317

332318
project_config_manager = mock.Mock()
333319
project_config_manager.get_project_config = mock.MagicMock(return_value=project_config)
@@ -337,7 +323,8 @@ def test_pull_projects_updates_lean_config() -> None:
337323
pull_manager = _create_pull_manager(api_client, project_config_manager, library_manager)
338324
pull_manager.pull_projects(cloud_projects, cloud_projects)
339325

340-
project_config.set.assert_called_with("organization-id", "123")
326+
project_config.set.assert_has_calls([mock.call("python-venv", 1), mock.call("organization-id", "123")],
327+
any_order=True)
341328

342329

343330
@pytest.mark.parametrize("test_platform, unsupported_character", [

0 commit comments

Comments
 (0)