Skip to content

Commit 553e9d4

Browse files
mguptahubPlane AIPrashant-Surya
authored
[INFRA-393] feat: add workflows, project templates, and ProjectFeature fields (#38)
* feat: add workflows, project templates, and ProjectFeature fields (INFRA-393) - Add Workflows API resource with list/create/update and sub-resources for WorkflowStates (attach/detach) and WorkflowTransitions (CRUD) - Add ProjectTemplates API resource with WorkItemTemplates and PageTemplates sub-resources, each supporting list/create/update/delete - Extend ProjectFeature model with missing fields: workflows, parallel_cycles, project_updates (was 7 fields, now all 10) - Export all new classes from plane/__init__.py - Add unit tests for workflows, project templates, and updated project feature tests Co-authored-by: Plane AI <noreply@plane.so> * fix: address CodeRabbit review comments on INFRA-393 - Add trailing slashes to all API endpoint paths (workflows, workflow states, workflow transitions, work item templates, page templates) - Sort __all__ in plane/api/workflows/__init__.py (Ruff RUF022) - Replace int(time.time()) with uuid4().hex in test fixtures to prevent name collisions on fast/parallel runs - Replace silent `except Exception: pass` with warnings.warn in test teardown to surface cleanup failures Co-authored-by: Plane AI <noreply@plane.so> --------- Co-authored-by: Plane AI <noreply@plane.so> Co-authored-by: Surya Prashanth <prashantsurya002@gmail.com>
1 parent ee179f9 commit 553e9d4

17 files changed

Lines changed: 1065 additions & 10 deletions

plane/__init__.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .api.milestones import Milestones
77
from .api.modules import Modules
88
from .api.pages import Pages
9+
from .api.project_templates import ProjectPageTemplates, ProjectTemplates, ProjectWorkItemTemplates
910
from .api.projects import Projects
1011
from .api.releases import Releases
1112
from .api.states import States
@@ -16,6 +17,7 @@
1617
from .api.work_item_relation_definitions import WorkItemRelationDefinitions
1718
from .api.work_item_types import WorkItemTypes
1819
from .api.work_items import WorkItems
20+
from .api.workflows import Workflows, WorkflowStates, WorkflowTransitions
1921
from .api.workspace_project_labels import WorkspaceProjectLabels
2022
from .api.workspace_project_states import WorkspaceProjectStates
2123
from .api.workspace_templates import WorkspaceTemplates
@@ -33,6 +35,24 @@
3335
)
3436
from .config import Configuration
3537
from .errors.errors import ConfigurationError, HttpError, PlaneError
38+
from .models.project_templates import (
39+
CreatePageTemplate,
40+
CreateWorkItemTemplate,
41+
PageTemplate,
42+
UpdatePageTemplate,
43+
UpdateWorkItemTemplate,
44+
WorkItemTemplate,
45+
)
46+
from .models.projects import ProjectFeature
47+
from .models.workflows import (
48+
AttachWorkflowStates,
49+
CreateWorkflow,
50+
CreateWorkflowTransition,
51+
UpdateWorkflow,
52+
UpdateWorkflowTransition,
53+
Workflow,
54+
WorkflowTransition,
55+
)
3656

3757
__all__ = [
3858
"PlaneClient",
@@ -56,6 +76,12 @@
5676
"Estimates",
5777
"Pages",
5878
"Workspaces",
79+
"Workflows",
80+
"WorkflowStates",
81+
"WorkflowTransitions",
82+
"ProjectTemplates",
83+
"ProjectWorkItemTemplates",
84+
"ProjectPageTemplates",
5985
"Releases",
6086
"WorkspaceTemplates",
6187
"WorkspaceWorkItemTypes",
@@ -70,4 +96,20 @@
7096
"OAuthTokenExchangeParams",
7197
"OAuthRefreshTokenParams",
7298
"OAuthClientCredentialsParams",
99+
# Workflow models
100+
"Workflow",
101+
"CreateWorkflow",
102+
"UpdateWorkflow",
103+
"AttachWorkflowStates",
104+
"WorkflowTransition",
105+
"CreateWorkflowTransition",
106+
"UpdateWorkflowTransition",
107+
"ProjectFeature",
108+
# Project template models
109+
"WorkItemTemplate",
110+
"CreateWorkItemTemplate",
111+
"UpdateWorkItemTemplate",
112+
"PageTemplate",
113+
"CreatePageTemplate",
114+
"UpdatePageTemplate",
73115
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .base import ProjectTemplates
2+
from .page_templates import ProjectPageTemplates
3+
from .work_item_templates import ProjectWorkItemTemplates
4+
5+
__all__ = ["ProjectTemplates", "ProjectWorkItemTemplates", "ProjectPageTemplates"]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import Any
2+
3+
from ..base_resource import BaseResource
4+
from .page_templates import ProjectPageTemplates
5+
from .work_item_templates import ProjectWorkItemTemplates
6+
7+
8+
class ProjectTemplates(BaseResource):
9+
"""API client for managing project-scoped templates (work items and pages)."""
10+
11+
def __init__(self, config: Any) -> None:
12+
super().__init__(config, "/workspaces/")
13+
14+
self.work_item_templates = ProjectWorkItemTemplates(config)
15+
self.page_templates = ProjectPageTemplates(config)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from typing import Any
2+
3+
from ...models.project_templates import (
4+
CreatePageTemplate,
5+
PageTemplate,
6+
UpdatePageTemplate,
7+
)
8+
from ..base_resource import BaseResource
9+
10+
11+
class ProjectPageTemplates(BaseResource):
12+
"""API client for managing page templates within a project."""
13+
14+
def __init__(self, config: Any) -> None:
15+
super().__init__(config, "/workspaces/")
16+
17+
def list(self, workspace_slug: str, project_id: str) -> list[PageTemplate]:
18+
"""List all page templates for a project.
19+
20+
Args:
21+
workspace_slug: The workspace slug identifier
22+
project_id: UUID of the project
23+
24+
Returns:
25+
List of page templates
26+
"""
27+
data = self._get(f"{workspace_slug}/projects/{project_id}/pages/templates/")
28+
items = data.get("results", data) if isinstance(data, dict) else data
29+
return [PageTemplate.model_validate(item) for item in items]
30+
31+
def create(
32+
self,
33+
workspace_slug: str,
34+
project_id: str,
35+
data: CreatePageTemplate,
36+
) -> PageTemplate:
37+
"""Create a new page template for a project.
38+
39+
Args:
40+
workspace_slug: The workspace slug identifier
41+
project_id: UUID of the project
42+
data: Template data
43+
44+
Returns:
45+
The created page template
46+
"""
47+
response = self._post(
48+
f"{workspace_slug}/projects/{project_id}/pages/templates/",
49+
data.model_dump(exclude_none=True),
50+
)
51+
return PageTemplate.model_validate(response)
52+
53+
def update(
54+
self,
55+
workspace_slug: str,
56+
project_id: str,
57+
template_id: str,
58+
data: UpdatePageTemplate,
59+
) -> PageTemplate:
60+
"""Update a page template by ID.
61+
62+
Args:
63+
workspace_slug: The workspace slug identifier
64+
project_id: UUID of the project
65+
template_id: UUID of the template
66+
data: Updated template data
67+
68+
Returns:
69+
The updated page template
70+
"""
71+
response = self._patch(
72+
f"{workspace_slug}/projects/{project_id}/pages/templates/{template_id}/",
73+
data.model_dump(exclude_none=True),
74+
)
75+
return PageTemplate.model_validate(response)
76+
77+
def delete(
78+
self,
79+
workspace_slug: str,
80+
project_id: str,
81+
template_id: str,
82+
) -> None:
83+
"""Delete a page template by ID.
84+
85+
Args:
86+
workspace_slug: The workspace slug identifier
87+
project_id: UUID of the project
88+
template_id: UUID of the template
89+
"""
90+
self._delete(f"{workspace_slug}/projects/{project_id}/pages/templates/{template_id}/")
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from typing import Any
2+
3+
from ...models.project_templates import (
4+
CreateWorkItemTemplate,
5+
UpdateWorkItemTemplate,
6+
WorkItemTemplate,
7+
)
8+
from ..base_resource import BaseResource
9+
10+
11+
class ProjectWorkItemTemplates(BaseResource):
12+
"""API client for managing work item templates within a project."""
13+
14+
def __init__(self, config: Any) -> None:
15+
super().__init__(config, "/workspaces/")
16+
17+
def list(self, workspace_slug: str, project_id: str) -> list[WorkItemTemplate]:
18+
"""List all work item templates for a project.
19+
20+
Args:
21+
workspace_slug: The workspace slug identifier
22+
project_id: UUID of the project
23+
24+
Returns:
25+
List of work item templates
26+
"""
27+
data = self._get(f"{workspace_slug}/projects/{project_id}/workitems/templates/")
28+
items = data.get("results", data) if isinstance(data, dict) else data
29+
return [WorkItemTemplate.model_validate(item) for item in items]
30+
31+
def create(
32+
self,
33+
workspace_slug: str,
34+
project_id: str,
35+
data: CreateWorkItemTemplate,
36+
) -> WorkItemTemplate:
37+
"""Create a new work item template for a project.
38+
39+
Args:
40+
workspace_slug: The workspace slug identifier
41+
project_id: UUID of the project
42+
data: Template data
43+
44+
Returns:
45+
The created work item template
46+
"""
47+
response = self._post(
48+
f"{workspace_slug}/projects/{project_id}/workitems/templates/",
49+
data.model_dump(exclude_none=True),
50+
)
51+
return WorkItemTemplate.model_validate(response)
52+
53+
def update(
54+
self,
55+
workspace_slug: str,
56+
project_id: str,
57+
template_id: str,
58+
data: UpdateWorkItemTemplate,
59+
) -> WorkItemTemplate:
60+
"""Update a work item template by ID.
61+
62+
Args:
63+
workspace_slug: The workspace slug identifier
64+
project_id: UUID of the project
65+
template_id: UUID of the template
66+
data: Updated template data
67+
68+
Returns:
69+
The updated work item template
70+
"""
71+
response = self._patch(
72+
f"{workspace_slug}/projects/{project_id}/workitems/templates/{template_id}",
73+
data.model_dump(exclude_none=True),
74+
)
75+
return WorkItemTemplate.model_validate(response)
76+
77+
def delete(
78+
self,
79+
workspace_slug: str,
80+
project_id: str,
81+
template_id: str,
82+
) -> None:
83+
"""Delete a work item template by ID.
84+
85+
Args:
86+
workspace_slug: The workspace slug identifier
87+
project_id: UUID of the project
88+
template_id: UUID of the template
89+
"""
90+
self._delete(f"{workspace_slug}/projects/{project_id}/workitems/templates/{template_id}/")

plane/api/projects.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,4 @@ def unarchive(self, workspace_slug: str, project_id: str) -> None:
149149
None (HTTP 204 No Content)
150150
"""
151151
self._delete(f"{workspace_slug}/projects/{project_id}/archive")
152+

plane/api/workflows/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .base import Workflows
2+
from .states import WorkflowStates
3+
from .transitions import WorkflowTransitions
4+
5+
__all__ = ["WorkflowStates", "WorkflowTransitions", "Workflows"]

plane/api/workflows/base.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from typing import Any
2+
3+
from ...models.workflows import CreateWorkflow, UpdateWorkflow, Workflow
4+
from ..base_resource import BaseResource
5+
from .states import WorkflowStates
6+
from .transitions import WorkflowTransitions
7+
8+
9+
class Workflows(BaseResource):
10+
"""API client for managing project workflows."""
11+
12+
def __init__(self, config: Any) -> None:
13+
super().__init__(config, "/workspaces/")
14+
15+
self.states = WorkflowStates(config)
16+
self.transitions = WorkflowTransitions(config)
17+
18+
def list(self, workspace_slug: str, project_id: str) -> list[Workflow]:
19+
"""List all workflows for a project.
20+
21+
Args:
22+
workspace_slug: The workspace slug identifier
23+
project_id: UUID of the project
24+
25+
Returns:
26+
List of workflows
27+
"""
28+
data = self._get(f"{workspace_slug}/projects/{project_id}/workflows/")
29+
items = data.get("results", data) if isinstance(data, dict) else data
30+
return [Workflow.model_validate(item) for item in items]
31+
32+
def create(
33+
self,
34+
workspace_slug: str,
35+
project_id: str,
36+
data: CreateWorkflow,
37+
) -> Workflow:
38+
"""Create a new workflow for a project.
39+
40+
Args:
41+
workspace_slug: The workspace slug identifier
42+
project_id: UUID of the project
43+
data: Workflow data
44+
45+
Returns:
46+
The created workflow
47+
"""
48+
response = self._post(
49+
f"{workspace_slug}/projects/{project_id}/workflows/",
50+
data.model_dump(exclude_none=True),
51+
)
52+
return Workflow.model_validate(response)
53+
54+
def update(
55+
self,
56+
workspace_slug: str,
57+
project_id: str,
58+
workflow_id: str,
59+
data: UpdateWorkflow,
60+
) -> Workflow:
61+
"""Update a workflow by ID.
62+
63+
Args:
64+
workspace_slug: The workspace slug identifier
65+
project_id: UUID of the project
66+
workflow_id: UUID of the workflow
67+
data: Updated workflow data
68+
69+
Returns:
70+
The updated workflow
71+
"""
72+
response = self._patch(
73+
f"{workspace_slug}/projects/{project_id}/workflows/{workflow_id}/",
74+
data.model_dump(exclude_none=True),
75+
)
76+
return Workflow.model_validate(response)

0 commit comments

Comments
 (0)