Skip to content

Commit 79c0a5a

Browse files
mguptahubPlane AI
andauthored
[INFRA-394] feat: add workspace-level API resources to SDK (#39)
* feat: add workspace-level API resources to SDK (INFRA-394) - Add WorkspaceTemplates resource with workitem, project, and page template sub-resources (list/create/update/delete each) - Add WorkspaceWorkItemTypes resource (list/create/update) with WorkspaceWorkItemTypeProperties sub-resource (list/link/unlink) - Add WorkspaceWorkItemProperties resource (list/create/update/delete) with WorkspaceWorkItemPropertyOptions sub-resource (list/create/update) - Add WorkspaceProjectLabels resource (list/create/update/delete) - Add WorkspaceProjectStates resource (list/create/update/delete) - Add WorkItemRelationDefinitions resource with is_default/is_active query param filters (list/create/update/delete) - Add Releases resource (list/create/update) with ReleaseTags, ReleaseLabels, and ReleaseItemLabels sub-resources - Add Pydantic models for all new resources - Register all 7 resources on PlaneClient - Export all new classes from plane/__init__.py - Add unit tests for all new resources Co-authored-by: Plane AI <noreply@plane.so> * fix: address CodeRabbit review comments on INFRA-394 PR #39 - rename ReleaseItemLabels.add/remove → create/delete (CRUD naming) - rename WorkspaceWorkItemTypeProperties.link/unlink → create/delete; create now accepts WorkspaceWorkItemTypePropertyLink DTO directly - test_releases: remove broad try/except around update (let it propagate); assert release.id and label.id are not None before use; update add/remove call sites to create/delete - test_work_item_relation_definitions: narrow except Exception → HttpError in teardown - test_workspace_templates: narrow all 3 teardown except Exception → HttpError - test_workspace_work_item_types: assert wit.id is not None before properties.list; add lifecycle test for property create/delete Co-authored-by: Plane AI <noreply@plane.so> * fix: let exceptions propagate in workspace WIT property lifecycle test - remove try/except HttpError in test_create_delete_property_link so API failures surface as test failures instead of warnings - remove redundant try/finally around update assertions in test_create_update_workspace_work_item_type - drop now-unused warnings and HttpError imports Co-authored-by: Plane AI <noreply@plane.so> --------- Co-authored-by: Plane AI <noreply@plane.so>
1 parent 88ade49 commit 79c0a5a

34 files changed

Lines changed: 1863 additions & 0 deletions

plane/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77
from .api.modules import Modules
88
from .api.pages import Pages
99
from .api.projects import Projects
10+
from .api.releases import Releases
1011
from .api.states import States
1112
from .api.stickies import Stickies
1213
from .api.teamspaces import Teamspaces
1314
from .api.users import Users
1415
from .api.work_item_properties import WorkItemProperties
16+
from .api.work_item_relation_definitions import WorkItemRelationDefinitions
1517
from .api.work_item_types import WorkItemTypes
1618
from .api.work_items import WorkItems
19+
from .api.workspace_project_labels import WorkspaceProjectLabels
20+
from .api.workspace_project_states import WorkspaceProjectStates
21+
from .api.workspace_templates import WorkspaceTemplates
22+
from .api.workspace_work_item_properties import WorkspaceWorkItemProperties
23+
from .api.workspace_work_item_types import WorkspaceWorkItemTypes
1724
from .api.workspaces import Workspaces
1825
from .client import (
1926
OAuthAuthorizationParams,
@@ -35,6 +42,7 @@
3542
"WorkItems",
3643
"WorkItemTypes",
3744
"WorkItemProperties",
45+
"WorkItemRelationDefinitions",
3846
"Projects",
3947
"Labels",
4048
"States",
@@ -48,6 +56,12 @@
4856
"Estimates",
4957
"Pages",
5058
"Workspaces",
59+
"Releases",
60+
"WorkspaceTemplates",
61+
"WorkspaceWorkItemTypes",
62+
"WorkspaceWorkItemProperties",
63+
"WorkspaceProjectLabels",
64+
"WorkspaceProjectStates",
5165
"PlaneError",
5266
"ConfigurationError",
5367
"HttpError",

plane/api/releases/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .base import Releases
2+
3+
__all__ = ["Releases"]

plane/api/releases/base.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from typing import Any
2+
3+
from ...models.releases import CreateRelease, Release, UpdateRelease
4+
from ..base_resource import BaseResource
5+
from .item_labels import ReleaseItemLabels
6+
from .labels import ReleaseLabels
7+
from .tags import ReleaseTags
8+
9+
10+
class Releases(BaseResource):
11+
"""API client for managing workspace-level releases."""
12+
13+
def __init__(self, config: Any) -> None:
14+
super().__init__(config, "/workspaces/")
15+
16+
# Initialize sub-resources
17+
self.tags = ReleaseTags(config)
18+
self.labels = ReleaseLabels(config)
19+
self.item_labels = ReleaseItemLabels(config)
20+
21+
def list(self, workspace_slug: str) -> list[Release]:
22+
"""List all releases in the workspace.
23+
24+
Args:
25+
workspace_slug: The workspace slug identifier
26+
"""
27+
response = self._get(f"{workspace_slug}/releases/")
28+
return [Release.model_validate(item) for item in response]
29+
30+
def create(self, workspace_slug: str, data: CreateRelease) -> Release:
31+
"""Create a new release in the workspace.
32+
33+
Args:
34+
workspace_slug: The workspace slug identifier
35+
data: Release data
36+
"""
37+
response = self._post(
38+
f"{workspace_slug}/releases/",
39+
data.model_dump(exclude_none=True),
40+
)
41+
return Release.model_validate(response)
42+
43+
def update(self, workspace_slug: str, release_id: str, data: UpdateRelease) -> Release:
44+
"""Update a release in the workspace.
45+
46+
Args:
47+
workspace_slug: The workspace slug identifier
48+
release_id: UUID of the release
49+
data: Updated release data
50+
"""
51+
response = self._patch(
52+
f"{workspace_slug}/releases/{release_id}/",
53+
data.model_dump(exclude_none=True),
54+
)
55+
return Release.model_validate(response)

plane/api/releases/item_labels.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from ...models.releases import AddReleaseItemLabel, ReleaseLabel
6+
from ..base_resource import BaseResource
7+
8+
9+
class ReleaseItemLabels(BaseResource):
10+
"""API client for managing labels on a specific release."""
11+
12+
def __init__(self, config: Any) -> None:
13+
super().__init__(config, "/workspaces/")
14+
15+
def list(self, workspace_slug: str, release_id: str) -> list[ReleaseLabel]:
16+
"""List labels assigned to a release.
17+
18+
Args:
19+
workspace_slug: The workspace slug identifier
20+
release_id: UUID of the release
21+
"""
22+
response = self._get(f"{workspace_slug}/releases/{release_id}/labels/")
23+
return [ReleaseLabel.model_validate(item) for item in response]
24+
25+
def create(
26+
self, workspace_slug: str, release_id: str, data: AddReleaseItemLabel
27+
) -> list[ReleaseLabel]:
28+
"""Add labels to a release.
29+
30+
Args:
31+
workspace_slug: The workspace slug identifier
32+
release_id: UUID of the release
33+
data: Label IDs to add
34+
"""
35+
response = self._post(
36+
f"{workspace_slug}/releases/{release_id}/labels/",
37+
data.model_dump(exclude_none=True),
38+
)
39+
return [ReleaseLabel.model_validate(item) for item in response]
40+
41+
def delete(self, workspace_slug: str, release_id: str, label_id: str) -> None:
42+
"""Remove a label from a release.
43+
44+
Args:
45+
workspace_slug: The workspace slug identifier
46+
release_id: UUID of the release
47+
label_id: UUID of the label to remove
48+
"""
49+
return self._delete(f"{workspace_slug}/releases/{release_id}/labels/{label_id}/")

plane/api/releases/labels.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Any
2+
3+
from ...models.releases import CreateReleaseLabel, ReleaseLabel
4+
from ..base_resource import BaseResource
5+
6+
7+
class ReleaseLabels(BaseResource):
8+
"""API client for managing release labels."""
9+
10+
def __init__(self, config: Any) -> None:
11+
super().__init__(config, "/workspaces/")
12+
13+
def list(self, workspace_slug: str) -> list[ReleaseLabel]:
14+
"""List all release labels in the workspace.
15+
16+
Args:
17+
workspace_slug: The workspace slug identifier
18+
"""
19+
response = self._get(f"{workspace_slug}/releases/labels/")
20+
return [ReleaseLabel.model_validate(item) for item in response]
21+
22+
def create(self, workspace_slug: str, data: CreateReleaseLabel) -> ReleaseLabel:
23+
"""Create a new release label in the workspace.
24+
25+
Args:
26+
workspace_slug: The workspace slug identifier
27+
data: Release label data
28+
"""
29+
response = self._post(
30+
f"{workspace_slug}/releases/labels/",
31+
data.model_dump(exclude_none=True),
32+
)
33+
return ReleaseLabel.model_validate(response)

plane/api/releases/tags.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Any
2+
3+
from ...models.releases import CreateReleaseTag, ReleaseTag
4+
from ..base_resource import BaseResource
5+
6+
7+
class ReleaseTags(BaseResource):
8+
"""API client for managing release tags."""
9+
10+
def __init__(self, config: Any) -> None:
11+
super().__init__(config, "/workspaces/")
12+
13+
def list(self, workspace_slug: str) -> list[ReleaseTag]:
14+
"""List all release tags in the workspace.
15+
16+
Args:
17+
workspace_slug: The workspace slug identifier
18+
"""
19+
response = self._get(f"{workspace_slug}/releases/tags/")
20+
return [ReleaseTag.model_validate(item) for item in response]
21+
22+
def create(self, workspace_slug: str, data: CreateReleaseTag) -> ReleaseTag:
23+
"""Create a new release tag in the workspace.
24+
25+
Args:
26+
workspace_slug: The workspace slug identifier
27+
data: Release tag data
28+
"""
29+
response = self._post(
30+
f"{workspace_slug}/releases/tags/",
31+
data.model_dump(exclude_none=True),
32+
)
33+
return ReleaseTag.model_validate(response)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from typing import Any
2+
3+
from ..models.work_item_relation_definitions import (
4+
CreateWorkItemRelationDefinition,
5+
UpdateWorkItemRelationDefinition,
6+
WorkItemRelationDefinition,
7+
)
8+
from .base_resource import BaseResource
9+
10+
11+
class WorkItemRelationDefinitions(BaseResource):
12+
"""API client for managing workspace-level work item relation definitions."""
13+
14+
def __init__(self, config: Any) -> None:
15+
super().__init__(config, "/workspaces/")
16+
17+
def list(
18+
self,
19+
workspace_slug: str,
20+
is_default: bool | None = None,
21+
is_active: bool | None = None,
22+
) -> list[WorkItemRelationDefinition]:
23+
"""List all work item relation definitions in the workspace.
24+
25+
Args:
26+
workspace_slug: The workspace slug identifier
27+
is_default: Optional filter by default status
28+
is_active: Optional filter by active status
29+
"""
30+
params: dict[str, Any] = {}
31+
if is_default is not None:
32+
params["is_default"] = str(is_default).lower()
33+
if is_active is not None:
34+
params["is_active"] = str(is_active).lower()
35+
36+
response = self._get(
37+
f"{workspace_slug}/work-item-relation-definitions/",
38+
params=params if params else None,
39+
)
40+
return [WorkItemRelationDefinition.model_validate(item) for item in response]
41+
42+
def create(
43+
self, workspace_slug: str, data: CreateWorkItemRelationDefinition
44+
) -> WorkItemRelationDefinition:
45+
"""Create a new work item relation definition in the workspace.
46+
47+
Args:
48+
workspace_slug: The workspace slug identifier
49+
data: Relation definition data
50+
"""
51+
response = self._post(
52+
f"{workspace_slug}/work-item-relation-definitions/",
53+
data.model_dump(exclude_none=True),
54+
)
55+
return WorkItemRelationDefinition.model_validate(response)
56+
57+
def update(
58+
self,
59+
workspace_slug: str,
60+
definition_id: str,
61+
data: UpdateWorkItemRelationDefinition,
62+
) -> WorkItemRelationDefinition:
63+
"""Update a work item relation definition in the workspace.
64+
65+
Args:
66+
workspace_slug: The workspace slug identifier
67+
definition_id: UUID of the relation definition
68+
data: Updated relation definition data
69+
"""
70+
response = self._patch(
71+
f"{workspace_slug}/work-item-relation-definitions/{definition_id}/",
72+
data.model_dump(exclude_none=True),
73+
)
74+
return WorkItemRelationDefinition.model_validate(response)
75+
76+
def delete(self, workspace_slug: str, definition_id: str) -> None:
77+
"""Delete a work item relation definition in the workspace.
78+
79+
Args:
80+
workspace_slug: The workspace slug identifier
81+
definition_id: UUID of the relation definition
82+
"""
83+
return self._delete(
84+
f"{workspace_slug}/work-item-relation-definitions/{definition_id}/"
85+
)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from typing import Any
2+
3+
from ..models.workspace_project_labels import (
4+
CreateWorkspaceProjectLabel,
5+
UpdateWorkspaceProjectLabel,
6+
WorkspaceProjectLabel,
7+
)
8+
from .base_resource import BaseResource
9+
10+
11+
class WorkspaceProjectLabels(BaseResource):
12+
"""API client for managing workspace-level project labels."""
13+
14+
def __init__(self, config: Any) -> None:
15+
super().__init__(config, "/workspaces/")
16+
17+
def list(self, workspace_slug: str) -> list[WorkspaceProjectLabel]:
18+
"""List all project labels in the workspace.
19+
20+
Args:
21+
workspace_slug: The workspace slug identifier
22+
"""
23+
response = self._get(f"{workspace_slug}/project-labels/")
24+
return [WorkspaceProjectLabel.model_validate(item) for item in response]
25+
26+
def create(
27+
self, workspace_slug: str, data: CreateWorkspaceProjectLabel
28+
) -> WorkspaceProjectLabel:
29+
"""Create a new project label in the workspace.
30+
31+
Args:
32+
workspace_slug: The workspace slug identifier
33+
data: Project label data
34+
"""
35+
response = self._post(
36+
f"{workspace_slug}/project-labels/",
37+
data.model_dump(exclude_none=True),
38+
)
39+
return WorkspaceProjectLabel.model_validate(response)
40+
41+
def update(
42+
self, workspace_slug: str, label_id: str, data: UpdateWorkspaceProjectLabel
43+
) -> WorkspaceProjectLabel:
44+
"""Update a project label in the workspace.
45+
46+
Args:
47+
workspace_slug: The workspace slug identifier
48+
label_id: UUID of the label
49+
data: Updated label data
50+
"""
51+
response = self._patch(
52+
f"{workspace_slug}/project-labels/{label_id}/",
53+
data.model_dump(exclude_none=True),
54+
)
55+
return WorkspaceProjectLabel.model_validate(response)
56+
57+
def delete(self, workspace_slug: str, label_id: str) -> None:
58+
"""Delete a project label in the workspace.
59+
60+
Args:
61+
workspace_slug: The workspace slug identifier
62+
label_id: UUID of the label
63+
"""
64+
return self._delete(f"{workspace_slug}/project-labels/{label_id}/")

0 commit comments

Comments
 (0)