Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions src/asyncplatform/resources/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,13 @@ async def importer(
*,
members: list[ProjectMember] | None = None,
preserve_existing_members: bool = True,
overwrite: bool = False,
) -> dict[str, Any]:
"""Import a project into the platform with optional member assignments.

Imports a project and optionally assigns specified groups or user accounts
as project members. Validates that the project doesn't already exist and
that all specified members exist in the platform before importing.
as project members. By default, validates that the project doesn't already
exist. Can optionally overwrite an existing project.

Args:
project: Complete project definition including name, description,
Expand All @@ -185,21 +186,30 @@ async def importer(
that were included in the imported project definition. If False,
removes all members from the imported project before adding the
specified members list
overwrite: If True, overwrites the project if it already exists in
the target environment. If False (default), raises an error if
the project already exists

Returns:
The imported project data including _id, name, and complete
configuration from the initial import response

Raises:
AsyncPlatformError: If a project with the same name already exists,
or if any specified member (group or account) does not exist,
or if a member has an invalid type
AsyncPlatformError: If overwrite is False and a project with the
same name already exists, or if any specified member (group or
account) does not exist, or if a member has an invalid type
HTTPError: If the import or patch operations fail
"""
project = copy.deepcopy(project)

# Ensure project is new
await self._ensure_project_is_new(project["name"])
# Check if project exists and handle based on overwrite flag
if not overwrite:
await self._ensure_project_is_new(project["name"])
else:
# Delete existing project if overwrite is True
existing_projects = await self.studio.find_projects(name=project["name"])
if existing_projects:
await self.studio.delete_project(existing_projects[0]["_id"])

# Import the project
result = await self.studio.import_project(project)
Expand Down
8 changes: 2 additions & 6 deletions src/asyncplatform/services/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,17 @@

from __future__ import annotations

import asyncio

from typing import Any

from asyncplatform import logging
from asyncplatform.services import ServiceBase


class Service(ServiceBase):

name: str = "help"

@logging.trace
async def get_openapi(self, url=None) -> dict[str, Any]:
"""
"""
async def get_openapi(self, url: str | None = None) -> dict[str, Any]:
""" """
res = await self.get("/help/openapi", params={"url": (url or "/")})
return res.json()
13 changes: 10 additions & 3 deletions src/asyncplatform/services/lifecycle_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ async def _fetch_all_paginated(
return results

tasks = [
self.get(path, params={"limit": min(limit, total - skip), "skip": skip, **filters})
self.get(
path,
params={"limit": min(limit, total - skip), "skip": skip, **filters},
)
for skip in range(limit, total, limit)
]

Expand Down Expand Up @@ -128,7 +131,9 @@ async def get_action_executions(self, **filters: Any) -> list[dict[str, Any]]:
Raises:
AsyncPlatformError: If any API request fails during retrieval
"""
return await self._fetch_all_paginated("/lifecycle-manager/action-executions", **filters)
return await self._fetch_all_paginated(
"/lifecycle-manager/action-executions", **filters
)

@logging.trace
async def get_action_execution(self, execution_id: str) -> dict[str, Any]:
Expand Down Expand Up @@ -185,7 +190,9 @@ async def get_resources(self, **filters: Any) -> list[dict[str, Any]]:
Raises:
AsyncPlatformError: If any API request fails during retrieval
"""
return await self._fetch_all_paginated("/lifecycle-manager/resources", **filters)
return await self._fetch_all_paginated(
"/lifecycle-manager/resources", **filters
)

@logging.trace
async def create_resource(self, resource_data: dict[str, Any]) -> dict[str, Any]:
Expand Down
143 changes: 143 additions & 0 deletions tests/unit/test_resources_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,149 @@ async def test_importer_preserve_existing_members_false_no_new_members(self):
# patch_project should not be called since members=None
mock_studio.patch_project.assert_not_called()

@pytest.mark.asyncio
async def test_importer_with_overwrite_true_deletes_existing(self):
"""Test that overwrite=True deletes existing project before importing.

Args:
None

Returns:
None

Raises:
None
"""
mock_client = MagicMock()
mock_studio = MagicMock()

# Mock finding existing project
mock_studio.find_projects = AsyncMock(
return_value=[{"_id": "existing_proj", "name": "Test Project"}]
)
# Mock deleting existing project
mock_studio.delete_project = AsyncMock(return_value={"message": "Deleted"})
# Mock importing new project
mock_studio.import_project = AsyncMock(
return_value={"_id": "new_proj", "name": "Test Project"}
)

mock_client.automation_studio = mock_studio

resource = Resource(mock_client)
project = {"name": "Test Project", "description": "Updated project"}

result = await resource.importer(project, overwrite=True)

# Verify existing project was found
mock_studio.find_projects.assert_called_once_with(name="Test Project")
# Verify existing project was deleted
mock_studio.delete_project.assert_called_once_with("existing_proj")
# Verify new project was imported
mock_studio.import_project.assert_called_once()
assert result["_id"] == "new_proj"
assert result["name"] == "Test Project"

@pytest.mark.asyncio
async def test_importer_with_overwrite_true_no_existing_project(self):
"""Test that overwrite=True works when no existing project exists.

Args:
None

Returns:
None

Raises:
None
"""
mock_client = MagicMock()
mock_studio = MagicMock()

# No existing project found
mock_studio.find_projects = AsyncMock(return_value=[])
mock_studio.delete_project = AsyncMock()
mock_studio.import_project = AsyncMock(
return_value={"_id": "proj1", "name": "Test Project"}
)

mock_client.automation_studio = mock_studio

resource = Resource(mock_client)
project = {"name": "Test Project", "description": "New project"}

result = await resource.importer(project, overwrite=True)

# Verify search was performed
mock_studio.find_projects.assert_called_once_with(name="Test Project")
# Verify no deletion occurred since project didn't exist
mock_studio.delete_project.assert_not_called()
# Verify project was imported
mock_studio.import_project.assert_called_once()
assert result["_id"] == "proj1"

@pytest.mark.asyncio
async def test_importer_with_overwrite_false_raises_on_existing(self):
"""Test that overwrite=False raises error when project exists.

Args:
None

Returns:
None

Raises:
None
"""
mock_client = MagicMock()
mock_studio = MagicMock()

mock_studio.find_projects = AsyncMock(
return_value=[{"_id": "existing", "name": "Test Project"}]
)
mock_client.automation_studio = mock_studio

resource = Resource(mock_client)
project = {"name": "Test Project", "description": "Test"}

# Test with explicit overwrite=False
with pytest.raises(exceptions.AsyncPlatformError) as exc_info:
await resource.importer(project, overwrite=False)

assert "Project `Test Project` already exists" in str(exc_info.value)
# Verify delete was never called
mock_studio.delete_project.assert_not_called()

@pytest.mark.asyncio
async def test_importer_overwrite_default_false(self):
"""Test that overwrite parameter defaults to False.

Args:
None

Returns:
None

Raises:
None
"""
mock_client = MagicMock()
mock_studio = MagicMock()

mock_studio.find_projects = AsyncMock(
return_value=[{"_id": "existing", "name": "Test Project"}]
)
mock_client.automation_studio = mock_studio

resource = Resource(mock_client)
project = {"name": "Test Project", "description": "Test"}

# Test without specifying overwrite (should default to False)
with pytest.raises(exceptions.AsyncPlatformError) as exc_info:
await resource.importer(project)

assert "Project `Test Project` already exists" in str(exc_info.value)


class TestDelete:
"""Test suite for delete method."""
Expand Down
9 changes: 7 additions & 2 deletions tests/unit/test_services_lifecycle_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,10 @@ async def test_fetch_all_paginated_propagates_exception(self, mock_get):
async def test_fetch_all_paginated_passes_filters(self, mock_get):
"""Test _fetch_all_paginated passes filter parameters correctly."""
mock_response = Mock()
mock_response.json.return_value = {"metadata": {"total": 1}, "data": [{"id": "1"}]}
mock_response.json.return_value = {
"metadata": {"total": 1},
"data": [{"id": "1"}],
}
mock_get.return_value = mock_response

ctx = context.Context()
Expand Down Expand Up @@ -367,7 +370,9 @@ async def test_edit_resource_returns_edited_data(self):
ctx.client = mock_client

service = Service(ctx)
edits = {"operations": [{"op": "replace", "path": "/name", "value": "EditedDevice"}]}
edits = {
"operations": [{"op": "replace", "path": "/name", "value": "EditedDevice"}]
}

result = await service.edit_resource("resource123", edits)

Expand Down