Skip to content

Commit

Permalink
Pull projects in the working organization only
Browse files Browse the repository at this point in the history
  • Loading branch information
jhonabreul committed Nov 8, 2022
1 parent fb5d11a commit a8be77d
Show file tree
Hide file tree
Showing 12 changed files with 60 additions and 38 deletions.
6 changes: 4 additions & 2 deletions lean/commands/cloud/pull.py
Expand Up @@ -43,10 +43,12 @@ def pull(project: Optional[str], pull_bootcamp: bool) -> None:
projects_to_pull = []
all_projects = None

organization_id = container.organization_manager.try_get_working_organization_id()

if project_id is not None:
projects_to_pull.append(api_client.projects.get(project_id))
projects_to_pull.append(api_client.projects.get(project_id, organization_id))
else:
all_projects = api_client.projects.get_all()
all_projects = api_client.projects.get_all(organization_id)
project_manager = container.project_manager
projects_to_pull = project_manager.get_projects_by_name_or_id(all_projects, project_name)

Expand Down
7 changes: 2 additions & 5 deletions lean/commands/data/download.py
Expand Up @@ -12,7 +12,7 @@
# limitations under the License.

from typing import Iterable, List, Optional
from click import command, option, confirm, pass_context, Context, Abort
from click import command, option, confirm, pass_context, Context

from lean.click import LeanCommand, ensure_options
from lean.container import container
Expand Down Expand Up @@ -408,10 +408,7 @@ def download(ctx: Context, dataset: Optional[str], overwrite: bool, **kwargs) ->
See the following url for the data that can be purchased and downloaded with this command:
https://www.quantconnect.com/datasets
"""
try:
organization = _get_organization()
except Abort:
return
organization = _get_organization()

is_interactive = dataset is None
if not is_interactive:
Expand Down
4 changes: 3 additions & 1 deletion lean/commands/delete_project.py
Expand Up @@ -27,9 +27,11 @@ def delete_project(project: str) -> None:
The project is selected by name or cloud id.
"""
organization_id = container.organization_manager.try_get_working_organization_id()

# Remove project from cloud
api_client = container.api_client
all_projects = api_client.projects.get_all()
all_projects = api_client.projects.get_all(organization_id)
project_manager = container.project_manager
logger = container.logger

Expand Down
2 changes: 1 addition & 1 deletion lean/commands/init.py
Expand Up @@ -31,7 +31,7 @@ def _get_organization_id(user_input: str) -> Tuple[str, str]:
If the user_input is an id (and it exists), it will be returned.
:param user_input: the input given by the user
:return: the organization id and name
:return the organization id and name
"""
from re import match
api_client = container.api_client
Expand Down
21 changes: 15 additions & 6 deletions lean/components/api/project_client.py
Expand Up @@ -27,24 +27,33 @@ def __init__(self, api_client: 'APIClient') -> None:
"""
self._api = api_client

def get(self, project_id: int) -> QCProject:
def get(self, project_id: int, organization_id: Optional[str]) -> QCProject:
"""Returns the details of a project.
:param project_id: the id of the project to retrieve the details of
:param organization_id: the id of the organization where the project is located
:return: the details of the specified project
"""
data = self._api.get("projects/read", {
"projectId": project_id
})
payload = {"projectId": project_id}
if organization_id is not None:
payload["organizationId"] = organization_id

data = self._api.get("projects/read", payload)

return self._process_project(QCProject(**data["projects"][0]))

def get_all(self) -> List[QCProject]:
def get_all(self, organization_id: Optional[str]) -> List[QCProject]:
"""Returns all the projects the user has access to.
:return: a list containing all the projects the user has access to
:param organization_id: the id of the organization where the projects are located
"""
data = self._api.get("projects/read")
payload = {}
if organization_id is not None:
payload["organizationId"] = organization_id

data = self._api.get("projects/read", payload)

return [self._process_project(QCProject(**project)) for project in data["projects"]]

def create(self, name: str, language: QCLanguage, organization_id: Optional[str]) -> QCProject:
Expand Down
18 changes: 12 additions & 6 deletions lean/components/cloud/cloud_project_manager.py
Expand Up @@ -17,6 +17,7 @@
from lean.components.cloud.pull_manager import PullManager
from lean.components.cloud.push_manager import PushManager
from lean.components.config.project_config_manager import ProjectConfigManager
from lean.components.util.organization_manager import OrganizationManager
from lean.components.util.path_manager import PathManager
from lean.models.api import QCProject

Expand All @@ -29,20 +30,23 @@ def __init__(self,
project_config_manager: ProjectConfigManager,
pull_manager: PullManager,
push_manager: PushManager,
path_manager: PathManager) -> None:
path_manager: PathManager,
organization_manager: OrganizationManager) -> None:
"""Creates a new PullManager instance.
:param api_client: the APIClient instance to use when communicating with the cloud
:param project_config_manager: the ProjectConfigManager instance to use
:param pull_manager: the PullManager instance to use
:param push_manager: the PushManager instance to use
:param path_manager: the PathManager instance to use when validating paths
:param organization_manager: the OrganizationManager instance to use to get the working organization id
"""
self._api_client = api_client
self._project_config_manager = project_config_manager
self._pull_manager = pull_manager
self._push_manager = push_manager
self._path_manager = path_manager
self._organization_manager = organization_manager

def get_cloud_project(self, input: str, push: bool) -> QCProject:
"""Retrieves the cloud project to use given a certain input and whether the local project needs to be pushed.
Expand All @@ -55,25 +59,27 @@ def get_cloud_project(self, input: str, push: bool) -> QCProject:
:param push: True if the local counterpart of the cloud project needs to be pushed
:return: the cloud project to use
"""
organization_id = self._organization_manager.try_get_working_organization_id()

# If the given input is a valid project directory, we try to use that project
local_path = Path.cwd() / input
if self._project_config_manager.try_get_project_config(local_path, self._path_manager):
if push:
self._push_manager.push_project([local_path])
self._push_manager.push_projects([local_path])

cloud_id = self._project_config_manager.get_project_config(local_path).get("cloud-id")
if cloud_id is None:
raise RuntimeError("Something went wrong while pushing the project to the cloud")

return self._api_client.projects.get(cloud_id)
return self._api_client.projects.get(cloud_id, organization_id)
else:
cloud_id = self._project_config_manager.get_project_config(local_path).get("cloud-id")
if cloud_id is not None:
return self._api_client.projects.get(cloud_id)
return self._api_client.projects.get(cloud_id, organization_id)

# If the given input is not a valid project directory, we look for a cloud project with a matching name or id
# If there are multiple, we use the first one
for cloud_project in self._api_client.projects.get_all():
for cloud_project in self._api_client.projects.get_all(organization_id):
if str(cloud_project.projectId) != input and cloud_project.name != input:
continue

Expand All @@ -84,7 +90,7 @@ def get_cloud_project(self, input: str, push: bool) -> QCProject:
local_path = self._pull_manager.get_local_project_path(cloud_project)
if local_path.exists():
self._push_manager.push_projects([local_path])
return self._api_client.projects.get(cloud_project.projectId)
return self._api_client.projects.get(cloud_project.projectId, organization_id)

return cloud_project

Expand Down
2 changes: 1 addition & 1 deletion lean/components/cloud/pull_manager.py
Expand Up @@ -60,7 +60,7 @@ def _get_libraries(self, project: QCProject, seen_projects: List[int] = None) ->
if library_id in seen_projects:
continue
seen_projects.append(library_id)
library = self._api_client.projects.get(library_id)
library = self._api_client.projects.get(library_id, project.organizationId)
libraries.append(library)
libraries.extend(self._get_libraries(library, seen_projects))

Expand Down
10 changes: 4 additions & 6 deletions lean/components/cloud/push_manager.py
Expand Up @@ -12,9 +12,7 @@
# limitations under the License.

from pathlib import Path
from typing import List, Optional, Dict

from click import Abort
from typing import List, Dict

from lean.components.api.api_client import APIClient
from lean.components.config.project_config_manager import ProjectConfigManager
Expand Down Expand Up @@ -107,7 +105,7 @@ def _push_project(self, project: Path, organization_id: str) -> None:
# Find the cloud project to push the files to
if cloud_id is not None:
# Project has cloud id which matches cloud project, update cloud project
cloud_project = self._get_cloud_project(cloud_id)
cloud_project = self._get_cloud_project(cloud_id, organization_id)
else:
# Project has invalid cloud id or no cloud id at all, create new cloud project
cloud_project = self._api_client.projects.create(project_name,
Expand Down Expand Up @@ -191,10 +189,10 @@ def _push_metadata(self, project: Path, cloud_project: QCProject) -> None:
self._api_client.projects.update(cloud_project.projectId, **update_args)
self._logger.info(f"Successfully updated {' and '.join(update_args.keys())} for '{cloud_project.name}'")

def _get_cloud_project(self, project_id: int) -> QCProject:
def _get_cloud_project(self, project_id: int, organization_id: str) -> QCProject:
project = next(iter(p for p in self._cloud_projects if p.projectId == project_id), None)
if project is None:
project = self._api_client.projects.get(project_id)
project = self._api_client.projects.get(project_id, organization_id)
self._cloud_projects.append(project)

return project
10 changes: 8 additions & 2 deletions lean/components/util/organization_manager.py
Expand Up @@ -30,13 +30,18 @@ def __init__(self, logger: Logger, lean_config_manager: LeanConfigManager) -> No
self._logger = logger
self._lean_config_manager = lean_config_manager

self._working_organization_id = None

def get_working_organization_id(self) -> Optional[str]:
"""Gets the id of the working organization in the current Lean CLI directory.
:return: the id of the working organization. None if the organization id was not found in the lean config
"""
lean_config = self._lean_config_manager.get_lean_config()
return lean_config.get("organization-id")
if self._working_organization_id is None:
lean_config = self._lean_config_manager.get_lean_config()
self._working_organization_id = lean_config.get("organization-id")

return self._working_organization_id

def try_get_working_organization_id(self) -> Optional[str]:
"""Gets the id of the working organization in the current Lean CLI directory.
Expand All @@ -58,3 +63,4 @@ def configure_working_organization_id(self, organization_id: str) -> None:
:param organization_id: the working organization di
"""
self._lean_config_manager.set_properties({"organization-id": organization_id})
self._working_organization_id = organization_id
4 changes: 3 additions & 1 deletion lean/container.py
Expand Up @@ -10,6 +10,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Union, Any

from lean.components.api.api_client import APIClient
Expand Down Expand Up @@ -133,7 +134,8 @@ def initialize(self,
self.project_config_manager,
self.pull_manager,
self.push_manager,
self.path_manager)
self.path_manager,
self.organization_manager)

self.docker_manager = docker_manager
if not self.docker_manager:
Expand Down
2 changes: 1 addition & 1 deletion tests/components/cloud/test_cloud_project_manager.py
Expand Up @@ -48,5 +48,5 @@ def test_get_cloud_project_pushing_new_project():

assert created_cloud_project == cloud_project

api_client.projects.get.assert_called_with(cloud_project.projectId)
api_client.projects.get.assert_called_with(cloud_project.projectId, "abc")
push_manager.push_projects.assert_called_once_with([Path.cwd() / "Python Project"])
12 changes: 6 additions & 6 deletions tests/components/util/test_push_manager.py
Expand Up @@ -23,7 +23,7 @@

def _create_organization_manager() -> mock.Mock:
organization_manager = mock.Mock()
organization_manager.try_get_working_organization_id = mock.MagicMock(return_value=None)
organization_manager.try_get_working_organization_id = mock.MagicMock(return_value="abc")
return organization_manager


Expand Down Expand Up @@ -122,9 +122,9 @@ def get_project(proj_id, *args):
any_order=True)

api_client.projects.create.assert_has_calls([
mock.call(csharp_library_path.relative_to(lean_cli_root_dir).as_posix(), QCLanguage.CSharp, None),
mock.call(python_library_path.relative_to(lean_cli_root_dir).as_posix(), QCLanguage.Python, None),
mock.call(project_path.relative_to(lean_cli_root_dir).as_posix(), QCLanguage.Python, None)
mock.call(csharp_library_path.relative_to(lean_cli_root_dir).as_posix(), QCLanguage.CSharp, "abc"),
mock.call(python_library_path.relative_to(lean_cli_root_dir).as_posix(), QCLanguage.Python, "abc"),
mock.call(project_path.relative_to(lean_cli_root_dir).as_posix(), QCLanguage.Python, "abc")
], any_order=True)

expected_update_call_arguments = [
Expand Down Expand Up @@ -219,7 +219,7 @@ def test_push_projects_adds_and_removes_libraries_simultaneously() -> None:

api_client = mock.Mock()

def projects_get_side_effect(proj_id: int) -> QCProject:
def projects_get_side_effect(proj_id: int, organization_id: int) -> QCProject:
return [p for p in [cloud_project, python_library_cloud_project, csharp_library_cloud_project]
if proj_id == p.projectId][0]

Expand All @@ -238,7 +238,7 @@ def projects_get_side_effect(proj_id: int) -> QCProject:

api_client.projects.create.assert_called_once_with(python_library_path.relative_to(lean_cli_root_dir).as_posix(),
QCLanguage.Python,
None)
"abc")

expected_update_call_arguments = [
{'project_id': python_library_id, 'libraries': []},
Expand Down

0 comments on commit a8be77d

Please sign in to comment.