Skip to content

Commit 88ade49

Browse files
[SILO-1276] feat: add work item page links, page listing, intake status, and archive methods (#36)
* feat: add work item pages API and models * feat: add methods to list workspace and project pages * fix: update params type in WorkItemPages to use Mapping for better type safety * chore: add update status method for intake work items and list archived work items method for work items * chore: add title field to CreateWorkItemLink * chore: add delete methods for workspace and project pages
1 parent 6e5389f commit 88ade49

14 files changed

Lines changed: 596 additions & 16 deletions

plane/api/intake.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,28 @@ def update(
9494
)
9595
return IntakeWorkItem.model_validate(response)
9696

97+
def update_status(
98+
self,
99+
workspace_slug: str,
100+
project_id: str,
101+
work_item_id: str,
102+
data: UpdateIntakeWorkItem,
103+
) -> IntakeWorkItem:
104+
"""Update the triage status of an intake work item.
105+
106+
Args:
107+
workspace_slug: The workspace slug identifier
108+
project_id: UUID of the project
109+
work_item_id: UUID of the work item (use the issue field from
110+
IntakeWorkItem response, not the intake work item ID)
111+
data: Triage data — status, snoozed_till, duplicate_to
112+
"""
113+
response = self._patch(
114+
f"{workspace_slug}/projects/{project_id}/intake-issues/{work_item_id}/status",
115+
data.model_dump(exclude_none=True),
116+
)
117+
return IntakeWorkItem.model_validate(response)
118+
97119
def delete(self, workspace_slug: str, project_id: str, work_item_id: str) -> None:
98120
"""Delete an intake work item by work item ID.
99121

plane/api/pages.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from collections.abc import Mapping
21
from typing import Any
32

43
from ..models.pages import CreatePage, Page, PaginatedPageResponse
@@ -10,6 +9,40 @@ class Pages(BaseResource):
109
def __init__(self, config: Any) -> None:
1110
super().__init__(config, "/workspaces/")
1211

12+
def list_workspace_pages(
13+
self,
14+
workspace_slug: str,
15+
params: PaginatedQueryParams | None = None,
16+
) -> PaginatedPageResponse:
17+
"""List all workspace pages.
18+
19+
Args:
20+
workspace_slug: The workspace slug identifier
21+
params: Optional pagination/query parameters
22+
"""
23+
query_params = params.model_dump(exclude_none=True) if params else None
24+
response = self._get(f"{workspace_slug}/pages", params=query_params)
25+
return PaginatedPageResponse.model_validate(response)
26+
27+
def list_project_pages(
28+
self,
29+
workspace_slug: str,
30+
project_id: str,
31+
params: PaginatedQueryParams | None = None,
32+
) -> PaginatedPageResponse:
33+
"""List all pages in a project.
34+
35+
Args:
36+
workspace_slug: The workspace slug identifier
37+
project_id: UUID of the project
38+
params: Optional pagination/query parameters
39+
"""
40+
query_params = params.model_dump(exclude_none=True) if params else None
41+
response = self._get(
42+
f"{workspace_slug}/projects/{project_id}/pages", params=query_params
43+
)
44+
return PaginatedPageResponse.model_validate(response)
45+
1346
def retrieve_workspace_page(
1447
self,
1548
workspace_slug: str,
@@ -83,3 +116,22 @@ def create_project_page(
83116
data.model_dump(exclude_none=True),
84117
)
85118
return Page.model_validate(response)
119+
120+
def delete_workspace_page(self, workspace_slug: str, page_id: str) -> None:
121+
"""Delete a workspace page by ID.
122+
123+
Args:
124+
workspace_slug: The workspace slug identifier
125+
page_id: UUID of the page
126+
"""
127+
return self._delete(f"{workspace_slug}/pages/{page_id}")
128+
129+
def delete_project_page(self, workspace_slug: str, project_id: str, page_id: str) -> None:
130+
"""Delete a project page by ID.
131+
132+
Args:
133+
workspace_slug: The workspace slug identifier
134+
project_id: UUID of the project
135+
page_id: UUID of the page
136+
"""
137+
return self._delete(f"{workspace_slug}/projects/{project_id}/pages/{page_id}")

plane/api/work_items/base.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .attachments import WorkItemAttachments
1919
from .comments import WorkItemComments
2020
from .links import WorkItemLinks
21+
from .pages import WorkItemPages
2122
from .relations import WorkItemRelations
2223
from .work_logs import WorkLogs
2324

@@ -33,6 +34,7 @@ def __init__(self, config: Any) -> None:
3334
self.comments = WorkItemComments(config)
3435
self.activities = WorkItemActivities(config)
3536
self.work_logs = WorkLogs(config)
37+
self.pages = WorkItemPages(config)
3638

3739
def create(self, workspace_slug: str, project_id: str, data: CreateWorkItem) -> WorkItem:
3840
"""Create a new work item.
@@ -237,6 +239,25 @@ def advanced_search(
237239
)
238240
return [AdvancedSearchResult.model_validate(item) for item in response]
239241

242+
def list_archived(
243+
self,
244+
workspace_slug: str,
245+
project_id: str,
246+
params: WorkItemQueryParams | None = None,
247+
) -> PaginatedWorkItemResponse:
248+
"""List archived work items in a project.
249+
250+
Args:
251+
workspace_slug: The workspace slug identifier
252+
project_id: UUID of the project
253+
params: Optional query parameters for filtering, ordering, and pagination
254+
"""
255+
query_params = params.model_dump(exclude_none=True) if params else None
256+
response = self._get(
257+
f"{workspace_slug}/projects/{project_id}/archived-work-items", params=query_params
258+
)
259+
return PaginatedWorkItemResponse.model_validate(response)
260+
240261
def archive(self, workspace_slug: str, project_id: str, work_item_id: str) -> None:
241262
"""Archive a work item.
242263

plane/api/work_items/pages.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from collections.abc import Mapping
2+
from typing import Any
3+
4+
from ...models.work_item_pages import (
5+
CreateWorkItemPage,
6+
PaginatedWorkItemPageResponse,
7+
WorkItemPage,
8+
)
9+
from ..base_resource import BaseResource
10+
11+
12+
class WorkItemPages(BaseResource):
13+
def __init__(self, config: Any) -> None:
14+
super().__init__(config, "/workspaces/")
15+
16+
def list(
17+
self,
18+
workspace_slug: str,
19+
project_id: str,
20+
work_item_id: str,
21+
params: Mapping[str, Any] | None = None,
22+
) -> PaginatedWorkItemPageResponse:
23+
"""List page links for a work item.
24+
25+
Args:
26+
workspace_slug: The workspace slug identifier
27+
project_id: UUID of the project
28+
work_item_id: UUID of the work item
29+
params: Optional query parameters
30+
"""
31+
response = self._get(
32+
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/pages",
33+
params=params,
34+
)
35+
return PaginatedWorkItemPageResponse.model_validate(response)
36+
37+
def retrieve(
38+
self,
39+
workspace_slug: str,
40+
project_id: str,
41+
work_item_id: str,
42+
work_item_page_id: str,
43+
) -> WorkItemPage:
44+
"""Retrieve a specific page link for a work item.
45+
46+
Args:
47+
workspace_slug: The workspace slug identifier
48+
project_id: UUID of the project
49+
work_item_id: UUID of the work item
50+
work_item_page_id: UUID of the work item page link
51+
"""
52+
response = self._get(
53+
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/pages/{work_item_page_id}"
54+
)
55+
return WorkItemPage.model_validate(response)
56+
57+
def create(
58+
self,
59+
workspace_slug: str,
60+
project_id: str,
61+
work_item_id: str,
62+
data: CreateWorkItemPage,
63+
) -> WorkItemPage:
64+
"""Link a page to a work item.
65+
66+
Args:
67+
workspace_slug: The workspace slug identifier
68+
project_id: UUID of the project
69+
work_item_id: UUID of the work item
70+
data: Page link data containing page_id
71+
"""
72+
response = self._post(
73+
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/pages",
74+
data.model_dump(exclude_none=True),
75+
)
76+
return WorkItemPage.model_validate(response)
77+
78+
def delete(
79+
self,
80+
workspace_slug: str,
81+
project_id: str,
82+
work_item_id: str,
83+
work_item_page_id: str,
84+
) -> None:
85+
"""Remove a page link from a work item.
86+
87+
Args:
88+
workspace_slug: The workspace slug identifier
89+
project_id: UUID of the project
90+
work_item_id: UUID of the work item
91+
work_item_page_id: UUID of the work item page link
92+
"""
93+
return self._delete(
94+
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/pages/{work_item_page_id}"
95+
)

plane/models/enums.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@
4545
"URL",
4646
"EMAIL",
4747
"FILE",
48+
"FORMULA",
4849
]
49-
RelationTypeEnum = Literal["ISSUE", "USER"]
50+
RelationTypeEnum = Literal["ISSUE", "USER", "RELEASE"]
5051

5152

5253
# Proper Enum classes for better type safety and IDE support
@@ -62,13 +63,15 @@ class PropertyType(Enum):
6263
URL = "URL"
6364
EMAIL = "EMAIL"
6465
FILE = "FILE"
66+
FORMULA = "FORMULA"
6567

6668

6769
class RelationType(Enum):
6870
"""Relation type enumeration."""
6971

7072
ISSUE = "ISSUE"
7173
USER = "USER"
74+
RELEASE = "RELEASE"
7275

7376

7477
class Priority(Enum):

plane/models/projects.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ class UpdateProject(BaseModel):
9292

9393
name: str | None = None
9494
description: str | None = None
95+
network: NetworkEnum | None = None
9596
project_lead: str | None = None
9697
default_assignee: str | None = None
9798
identifier: str | None = None

plane/models/work_item_pages.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from typing import Any
2+
3+
from pydantic import BaseModel, ConfigDict
4+
5+
from .pagination import PaginatedResponse
6+
7+
8+
class WorkItemPageLite(BaseModel):
9+
"""Nested page info returned inside a WorkItemPage response."""
10+
11+
model_config = ConfigDict(extra="allow", populate_by_name=True)
12+
13+
id: str | None = None
14+
name: str | None = None
15+
created_at: str | None = None
16+
updated_at: str | None = None
17+
created_by: str | None = None
18+
is_global: bool | None = None
19+
logo_props: Any | None = None
20+
21+
22+
class WorkItemPage(BaseModel):
23+
"""Work item to page link."""
24+
25+
model_config = ConfigDict(extra="allow", populate_by_name=True)
26+
27+
id: str | None = None
28+
page: WorkItemPageLite | None = None
29+
issue: str | None = None
30+
project: str | None = None
31+
workspace: str | None = None
32+
created_at: str | None = None
33+
updated_at: str | None = None
34+
created_by: str | None = None
35+
36+
37+
class CreateWorkItemPage(BaseModel):
38+
"""Request model for linking a page to a work item."""
39+
40+
model_config = ConfigDict(extra="ignore", populate_by_name=True)
41+
42+
page_id: str
43+
44+
45+
class PaginatedWorkItemPageResponse(PaginatedResponse):
46+
"""Paginated response for work item page links."""
47+
48+
model_config = ConfigDict(extra="allow", populate_by_name=True)
49+
50+
results: list[WorkItemPage]

plane/models/work_item_properties.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ class WorkItemProperty(BaseModel):
4444
options: list["WorkItemPropertyOption"] | None = None
4545

4646
@field_serializer("property_type")
47-
def serialize_property_type(self, value: PropertyType) -> str:
47+
def serialize_property_type(self, value: PropertyType) -> str | None:
4848
return value.value if value else None
4949

5050
@field_serializer("relation_type")
51-
def serialize_relation_type(self, value: RelationType) -> str:
51+
def serialize_relation_type(self, value: RelationType) -> str | None:
5252
return value.value if value else None
5353

5454

@@ -72,11 +72,11 @@ class CreateWorkItemProperty(BaseModel):
7272
options: list["CreateWorkItemPropertyOption"] | None = None
7373

7474
@field_serializer("property_type")
75-
def serialize_property_type(self, value: PropertyType) -> str:
75+
def serialize_property_type(self, value: PropertyType) -> str | None:
7676
return value.value if value else None
7777

7878
@field_serializer("relation_type")
79-
def serialize_relation_type(self, value: RelationType) -> str:
79+
def serialize_relation_type(self, value: RelationType) -> str | None:
8080
return value.value if value else None
8181

8282
@model_validator(mode="after")
@@ -131,11 +131,11 @@ class UpdateWorkItemProperty(BaseModel):
131131
external_id: str | None = None
132132

133133
@field_serializer("property_type")
134-
def serialize_property_type(self, value: PropertyType) -> str:
134+
def serialize_property_type(self, value: PropertyType) -> str | None:
135135
return value.value if value else None
136136

137137
@field_serializer("relation_type")
138-
def serialize_relation_type(self, value: RelationType) -> str:
138+
def serialize_relation_type(self, value: RelationType) -> str | None:
139139
return value.value if value else None
140140

141141
@model_validator(mode="after")

plane/models/work_items.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ class CreateWorkItemLink(BaseModel):
368368
model_config = ConfigDict(extra="ignore", populate_by_name=True)
369369

370370
url: str
371+
title: str | None = None
371372

372373

373374
class UpdateWorkItemLink(BaseModel):
@@ -376,6 +377,7 @@ class UpdateWorkItemLink(BaseModel):
376377
model_config = ConfigDict(extra="ignore", populate_by_name=True)
377378

378379
url: str | None = None
380+
title: str | None = None
379381

380382

381383
class WorkItemAttachment(BaseModel):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "plane-sdk"
7-
version = "0.2.11"
7+
version = "0.2.12"
88
description = "Python SDK for Plane API"
99
readme = "README.md"
1010
requires-python = ">=3.10"

0 commit comments

Comments
 (0)