-
Notifications
You must be signed in to change notification settings - Fork 6
[SILO-1143] feat: add epics write endpoints #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,16 @@ | ||||||||||||||||||||||
| from __future__ import annotations | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from typing import Any | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from ..models.epics import Epic, PaginatedEpicResponse | ||||||||||||||||||||||
| from ..models.epics import ( | ||||||||||||||||||||||
| AddEpicWorkItems, | ||||||||||||||||||||||
| CreateEpic, | ||||||||||||||||||||||
| Epic, | ||||||||||||||||||||||
| EpicIssue, | ||||||||||||||||||||||
| PaginatedEpicIssueResponse, | ||||||||||||||||||||||
| PaginatedEpicResponse, | ||||||||||||||||||||||
| UpdateEpic, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| from ..models.query_params import PaginatedQueryParams, RetrieveQueryParams | ||||||||||||||||||||||
| from .base_resource import BaseResource | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
@@ -9,6 +19,69 @@ class Epics(BaseResource): | |||||||||||||||||||||
| def __init__(self, config: Any) -> None: | ||||||||||||||||||||||
| super().__init__(config, "/workspaces/") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def create(self, workspace_slug: str, project_id: str, data: CreateEpic) -> Epic: | ||||||||||||||||||||||
| """Create a new epic in a project. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Args: | ||||||||||||||||||||||
| workspace_slug: The workspace slug identifier | ||||||||||||||||||||||
| project_id: UUID of the project | ||||||||||||||||||||||
| data: Epic creation data | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| # enable epics feature flag | ||||||||||||||||||||||
| response = self._post( | ||||||||||||||||||||||
| f"{workspace_slug}/projects/{project_id}/epics", | ||||||||||||||||||||||
| data.model_dump(exclude_none=True), | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| return Epic.model_validate(response) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def retrieve( | ||||||||||||||||||||||
| self, | ||||||||||||||||||||||
| workspace_slug: str, | ||||||||||||||||||||||
| project_id: str, | ||||||||||||||||||||||
| epic_id: str, | ||||||||||||||||||||||
| params: RetrieveQueryParams | None = None, | ||||||||||||||||||||||
| ) -> Epic: | ||||||||||||||||||||||
| """Retrieve an epic by ID. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Args: | ||||||||||||||||||||||
| workspace_slug: The workspace slug identifier | ||||||||||||||||||||||
| project_id: UUID of the project | ||||||||||||||||||||||
| epic_id: UUID of the epic | ||||||||||||||||||||||
| params: Optional query parameters for expand, fields, etc. | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| query_params = params.model_dump(exclude_none=True) if params else None | ||||||||||||||||||||||
| response = self._get( | ||||||||||||||||||||||
| f"{workspace_slug}/projects/{project_id}/epics/{epic_id}", params=query_params | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| return Epic.model_validate(response) | ||||||||||||||||||||||
|
Comment on lines
+52
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add trailing slash to endpoint URLs. The retrieve, update, and delete endpoints are missing trailing slashes. 🔧 Proposed fixes for lines 54, 70, 83 response = self._get(
- f"{workspace_slug}/projects/{project_id}/epics/{epic_id}", params=query_params
+ f"{workspace_slug}/projects/{project_id}/epics/{epic_id}/", params=query_params
) response = self._patch(
- f"{workspace_slug}/projects/{project_id}/epics/{epic_id}",
+ f"{workspace_slug}/projects/{project_id}/epics/{epic_id}/",
data.model_dump(exclude_none=True),
)- self._delete(f"{workspace_slug}/projects/{project_id}/epics/{epic_id}")
+ self._delete(f"{workspace_slug}/projects/{project_id}/epics/{epic_id}/")As per coding guidelines: "All API endpoints should end with a trailing 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def update( | ||||||||||||||||||||||
| self, workspace_slug: str, project_id: str, epic_id: str, data: UpdateEpic | ||||||||||||||||||||||
| ) -> Epic: | ||||||||||||||||||||||
| """Partially update an existing epic. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Args: | ||||||||||||||||||||||
| workspace_slug: The workspace slug identifier | ||||||||||||||||||||||
| project_id: UUID of the project | ||||||||||||||||||||||
| epic_id: UUID of the epic | ||||||||||||||||||||||
| data: Epic update data | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| response = self._patch( | ||||||||||||||||||||||
| f"{workspace_slug}/projects/{project_id}/epics/{epic_id}", | ||||||||||||||||||||||
| data.model_dump(exclude_none=True), | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| return Epic.model_validate(response) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def delete(self, workspace_slug: str, project_id: str, epic_id: str) -> None: | ||||||||||||||||||||||
| """Delete an epic. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Args: | ||||||||||||||||||||||
| workspace_slug: The workspace slug identifier | ||||||||||||||||||||||
| project_id: UUID of the project | ||||||||||||||||||||||
| epic_id: UUID of the epic | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| self._delete(f"{workspace_slug}/projects/{project_id}/epics/{epic_id}") | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def list( | ||||||||||||||||||||||
| self, | ||||||||||||||||||||||
| workspace_slug: str, | ||||||||||||||||||||||
|
|
@@ -26,23 +99,45 @@ def list( | |||||||||||||||||||||
| response = self._get(f"{workspace_slug}/projects/{project_id}/epics", params=query_params) | ||||||||||||||||||||||
| return PaginatedEpicResponse.model_validate(response) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def retrieve( | ||||||||||||||||||||||
| def list_issues( | ||||||||||||||||||||||
| self, | ||||||||||||||||||||||
| workspace_slug: str, | ||||||||||||||||||||||
| project_id: str, | ||||||||||||||||||||||
| epic_id: str, | ||||||||||||||||||||||
| params: RetrieveQueryParams | None = None, | ||||||||||||||||||||||
| ) -> Epic: | ||||||||||||||||||||||
| """Retrieve an epic by ID. | ||||||||||||||||||||||
| params: PaginatedQueryParams | None = None, | ||||||||||||||||||||||
| ) -> PaginatedEpicIssueResponse: | ||||||||||||||||||||||
| """List work items under an epic. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Args: | ||||||||||||||||||||||
| workspace_slug: The workspace slug identifier | ||||||||||||||||||||||
| project_id: UUID of the project | ||||||||||||||||||||||
| epic_id: UUID of the epic | ||||||||||||||||||||||
| params: Optional query parameters for expand, fields, etc. | ||||||||||||||||||||||
| params: Optional query parameters for pagination | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| query_params = params.model_dump(exclude_none=True) if params else None | ||||||||||||||||||||||
| response = self._get( | ||||||||||||||||||||||
| f"{workspace_slug}/projects/{project_id}/epics/{epic_id}", params=query_params | ||||||||||||||||||||||
| f"{workspace_slug}/projects/{project_id}/epics/{epic_id}/issues", | ||||||||||||||||||||||
| params=query_params, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
Comment on lines
118
to
121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add trailing slash to endpoint URL. 🔧 Proposed fix response = self._get(
- f"{workspace_slug}/projects/{project_id}/epics/{epic_id}/issues",
+ f"{workspace_slug}/projects/{project_id}/epics/{epic_id}/issues/",
params=query_params,
)As per coding guidelines: "All API endpoints should end with a trailing 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| return Epic.model_validate(response) | ||||||||||||||||||||||
| return PaginatedEpicIssueResponse.model_validate(response) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def add_issues( | ||||||||||||||||||||||
| self, | ||||||||||||||||||||||
| workspace_slug: str, | ||||||||||||||||||||||
| project_id: str, | ||||||||||||||||||||||
| epic_id: str, | ||||||||||||||||||||||
| data: AddEpicWorkItems, | ||||||||||||||||||||||
| ) -> list[EpicIssue]: | ||||||||||||||||||||||
| """Add work items as sub-issues under an epic. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Args: | ||||||||||||||||||||||
| workspace_slug: The workspace slug identifier | ||||||||||||||||||||||
| project_id: UUID of the project | ||||||||||||||||||||||
| epic_id: UUID of the epic | ||||||||||||||||||||||
| data: Work item IDs to add | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
| response = self._post( | ||||||||||||||||||||||
| f"{workspace_slug}/projects/{project_id}/epics/{epic_id}/issues", | ||||||||||||||||||||||
| data.model_dump(exclude_none=True), | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| return [EpicIssue.model_validate(item) for item in response] | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,14 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Unit tests for Epics API resource (smoke tests with real HTTP requests).""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from datetime import datetime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.client import PlaneClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.models.projects import Project | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.models.epics import AddEpicWorkItems, CreateEpic, Epic, UpdateEpic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.models.projects import Project, ProjectFeature | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.models.query_params import PaginatedQueryParams | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from plane.models.work_items import CreateWorkItem | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class TestEpicsAPI: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -28,3 +34,82 @@ def test_list_epics_with_params( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert hasattr(response, "results") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(response.results) <= 10 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class TestEpicsAPICRUD: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test Epics API CRUD operations.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @pytest.fixture | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def epic( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, client: PlaneClient, workspace_slug: str, project: Project | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> Epic: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Create a test epic and yield it, then delete it.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = CreateEpic(name=f"Test Epic {timestamp}", priority="high") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| epic = client.epics.create(workspace_slug, project.id, data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield epic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.epics.delete(workspace_slug, project.id, epic.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_create_epic( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, client: PlaneClient, workspace_slug: str, project: Project | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test creating an epic.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = CreateEpic(name=f"Test Epic {timestamp}") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.projects.update_features(workspace_slug, project.id, ProjectFeature(epics=True)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| epic = client.epics.create(workspace_slug, project.id, data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert epic is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert epic.id is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert epic.name == data.name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing cleanup for created epic. This test creates an epic directly without using the 🧹 Proposed fix to add cleanup def test_create_epic(
self, client: PlaneClient, workspace_slug: str, project: Project
) -> None:
"""Test creating an epic."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
data = CreateEpic(name=f"Test Epic {timestamp}")
client.projects.update_features(workspace_slug, project.id, ProjectFeature(epics=True))
epic = client.epics.create(workspace_slug, project.id, data)
- assert epic is not None
- assert epic.id is not None
- assert epic.name == data.name
+ try:
+ assert epic is not None
+ assert epic.id is not None
+ assert epic.name == data.name
+ finally:
+ try:
+ client.epics.delete(workspace_slug, project.id, epic.id)
+ except Exception:
+ pass📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_retrieve_epic( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, client: PlaneClient, workspace_slug: str, project: Project, epic: Epic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test retrieving an epic.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| retrieved = client.epics.retrieve(workspace_slug, project.id, epic.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert retrieved is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert retrieved.id == epic.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert retrieved.name == epic.name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_update_epic( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, client: PlaneClient, workspace_slug: str, project: Project, epic: Epic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test updating an epic.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| update_data = UpdateEpic(name="Updated Epic Name") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updated = client.epics.update(workspace_slug, project.id, epic.id, update_data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert updated is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert updated.id == epic.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert updated.name == "Updated Epic Name" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_list_epic_issues( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, client: PlaneClient, workspace_slug: str, project: Project, epic: Epic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test listing work items under an epic.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response = client.epics.list_issues(workspace_slug, project.id, epic.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert response is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert hasattr(response, "results") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert isinstance(response.results, list) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def test_add_issues_to_epic( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, client: PlaneClient, workspace_slug: str, project: Project, epic: Epic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Test adding work items to an epic.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| work_item = client.work_items.create( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspace_slug, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| project.id, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CreateWorkItem(name="Test Work Item for Epic"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = AddEpicWorkItems(work_item_ids=[work_item.id]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| added = client.epics.add_issues(workspace_slug, project.id, epic.id, data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert added is not None | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert isinstance(added, list) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert len(added) == 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| assert added[0].parent == epic.id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| finally: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| client.work_items.delete(workspace_slug, project.id, work_item.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| except Exception: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove orphaned comment and add trailing slash to endpoint.
The comment "enable epics feature flag" has no corresponding code. Additionally, the endpoint URL should end with a trailing
/per coding guidelines.🔧 Proposed fix
As per coding guidelines: "All API endpoints should end with a trailing
/"🤖 Prompt for AI Agents