Skip to content

Commit ee179f9

Browse files
feat: project-level property APIs, workspace CRUD completions, and resilient work item models (#42)
* refactor: make work item model fields optional and add project-level work item property management API endpoints * test: add unit tests for workspace-level and project-level work item property and type operations * test: skip work item property creation tests when project-level properties are disabled by workspace settings
1 parent 3e0e257 commit ee179f9

11 files changed

Lines changed: 454 additions & 18 deletions

File tree

plane/api/work_item_properties/base.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from collections.abc import Mapping
24
from typing import Any
35

@@ -90,14 +92,127 @@ def delete(
9092
f"{workspace_slug}/projects/{project_id}/work-item-types/{type_id}/work-item-properties/{work_item_property_id}"
9193
)
9294

95+
def list_project(
96+
self,
97+
workspace_slug: str,
98+
project_id: str,
99+
params: Mapping[str, Any] | None = None,
100+
) -> list[WorkItemProperty]:
101+
"""List all work item properties for a project regardless of work item type.
102+
103+
Args:
104+
workspace_slug: The workspace slug identifier
105+
project_id: UUID of the project
106+
params: Optional query parameters
107+
"""
108+
response = self._get(
109+
f"{workspace_slug}/projects/{project_id}/work-item-properties",
110+
params=params,
111+
)
112+
return [WorkItemProperty.model_validate(item) for item in response]
113+
114+
def create_project(
115+
self, workspace_slug: str, project_id: str, data: CreateWorkItemProperty
116+
) -> WorkItemProperty:
117+
"""Create a project-level work item property (not linked to a specific type).
118+
119+
Args:
120+
workspace_slug: The workspace slug identifier
121+
project_id: UUID of the project
122+
data: Work item property data
123+
"""
124+
response = self._post(
125+
f"{workspace_slug}/projects/{project_id}/work-item-properties",
126+
data.model_dump(exclude_none=True),
127+
)
128+
return WorkItemProperty.model_validate(response)
129+
130+
def retrieve_project(
131+
self, workspace_slug: str, project_id: str, property_id: str
132+
) -> WorkItemProperty:
133+
"""Retrieve a project-level work item property by ID.
134+
135+
Args:
136+
workspace_slug: The workspace slug identifier
137+
project_id: UUID of the project
138+
property_id: UUID of the property
139+
"""
140+
response = self._get(
141+
f"{workspace_slug}/projects/{project_id}/work-item-properties/{property_id}"
142+
)
143+
return WorkItemProperty.model_validate(response)
144+
145+
def update_project(
146+
self, workspace_slug: str, project_id: str, property_id: str, data: UpdateWorkItemProperty
147+
) -> WorkItemProperty:
148+
"""Update a project-level work item property by ID.
149+
150+
Args:
151+
workspace_slug: The workspace slug identifier
152+
project_id: UUID of the project
153+
property_id: UUID of the property
154+
data: Updated property data
155+
"""
156+
response = self._patch(
157+
f"{workspace_slug}/projects/{project_id}/work-item-properties/{property_id}",
158+
data.model_dump(exclude_none=True),
159+
)
160+
return WorkItemProperty.model_validate(response)
161+
162+
def delete_project(
163+
self, workspace_slug: str, project_id: str, property_id: str
164+
) -> None:
165+
"""Delete a project-level work item property by ID.
166+
167+
Args:
168+
workspace_slug: The workspace slug identifier
169+
project_id: UUID of the project
170+
property_id: UUID of the property
171+
"""
172+
return self._delete(
173+
f"{workspace_slug}/projects/{project_id}/work-item-properties/{property_id}"
174+
)
175+
176+
def attach_to_type(
177+
self, workspace_slug: str, project_id: str, type_id: str, property_ids: list[str]
178+
) -> list[str]:
179+
"""Attach existing project-level properties to a work item type.
180+
181+
Args:
182+
workspace_slug: The workspace slug identifier
183+
project_id: UUID of the project
184+
type_id: UUID of the work item type
185+
property_ids: List of property UUIDs to attach
186+
"""
187+
response = self._post(
188+
f"{workspace_slug}/projects/{project_id}/work-item-types/{type_id}/properties",
189+
{"properties": property_ids},
190+
)
191+
return response.get("properties", [])
192+
193+
def detach_from_type(
194+
self, workspace_slug: str, project_id: str, type_id: str, property_id: str
195+
) -> None:
196+
"""Detach a property from a work item type.
197+
198+
Args:
199+
workspace_slug: The workspace slug identifier
200+
project_id: UUID of the project
201+
type_id: UUID of the work item type
202+
property_id: UUID of the property to detach
203+
"""
204+
return self._delete(
205+
f"{workspace_slug}/projects/{project_id}/work-item-types/{type_id}/properties/{property_id}"
206+
)
207+
93208
def list(
94209
self,
95210
workspace_slug: str,
96211
project_id: str,
97212
type_id: str,
98213
params: Mapping[str, Any] | None = None,
99214
) -> list[WorkItemProperty]:
100-
"""List work item properties with optional filtering parameters.
215+
"""List project-level work item properties for a type.
101216
102217
Args:
103218
workspace_slug: The workspace slug identifier
@@ -110,3 +225,4 @@ def list(
110225
params=params,
111226
)
112227
return [WorkItemProperty.model_validate(item) for item in response]
228+

plane/api/work_items/base.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,23 @@ def list(
177177
)
178178
return PaginatedWorkItemResponse.model_validate(response)
179179

180+
def list_workspace(
181+
self,
182+
workspace_slug: str,
183+
params: WorkItemQueryParams | None = None,
184+
) -> PaginatedWorkItemResponse:
185+
"""List work items across the entire workspace.
186+
187+
Args:
188+
workspace_slug: The workspace slug identifier
189+
params: Optional query parameters for filtering, ordering, and pagination
190+
"""
191+
query_params = params.model_dump(exclude_none=True) if params else None
192+
response = self._get(
193+
f"{workspace_slug}/work-items", params=query_params
194+
)
195+
return PaginatedWorkItemResponse.model_validate(response)
196+
180197
def search(
181198
self,
182199
workspace_slug: str,

plane/api/workspace_work_item_properties/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ def update(
5858
)
5959
return WorkItemProperty.model_validate(response)
6060

61+
def retrieve(self, workspace_slug: str, property_id: str) -> WorkItemProperty:
62+
"""Retrieve a workspace work item property by ID.
63+
64+
Args:
65+
workspace_slug: The workspace slug identifier
66+
property_id: UUID of the work item property
67+
"""
68+
response = self._get(f"{workspace_slug}/work-item-properties/{property_id}/")
69+
return WorkItemProperty.model_validate(response)
70+
6171
def delete(self, workspace_slug: str, property_id: str) -> None:
6272
"""Delete a work item property in the workspace.
6373

plane/api/workspace_work_item_properties/options.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,30 @@ def update(
6565
data.model_dump(exclude_none=True),
6666
)
6767
return WorkItemPropertyOption.model_validate(response)
68+
69+
def retrieve(
70+
self, workspace_slug: str, property_id: str, option_id: str
71+
) -> WorkItemPropertyOption:
72+
"""Retrieve an option for a workspace work item property.
73+
74+
Args:
75+
workspace_slug: The workspace slug identifier
76+
property_id: UUID of the work item property
77+
option_id: UUID of the option
78+
"""
79+
response = self._get(
80+
f"{workspace_slug}/work-item-properties/{property_id}/options/{option_id}/"
81+
)
82+
return WorkItemPropertyOption.model_validate(response)
83+
84+
def delete(self, workspace_slug: str, property_id: str, option_id: str) -> None:
85+
"""Delete an option from a workspace work item property.
86+
87+
Args:
88+
workspace_slug: The workspace slug identifier
89+
property_id: UUID of the work item property
90+
option_id: UUID of the option
91+
"""
92+
return self._delete(
93+
f"{workspace_slug}/work-item-properties/{property_id}/options/{option_id}/"
94+
)

plane/api/workspace_work_item_types/base.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ def create(self, workspace_slug: str, data: CreateWorkItemType) -> WorkItemType:
3636
)
3737
return WorkItemType.model_validate(response)
3838

39+
def retrieve(self, workspace_slug: str, type_id: str) -> WorkItemType:
40+
"""Retrieve a workspace work item type by ID.
41+
42+
Args:
43+
workspace_slug: The workspace slug identifier
44+
type_id: UUID of the work item type
45+
"""
46+
response = self._get(f"{workspace_slug}/work-item-types/{type_id}/")
47+
return WorkItemType.model_validate(response)
48+
3949
def update(
4050
self, workspace_slug: str, type_id: str, data: UpdateWorkItemType
4151
) -> WorkItemType:
@@ -51,3 +61,12 @@ def update(
5161
data.model_dump(exclude_none=True),
5262
)
5363
return WorkItemType.model_validate(response)
64+
65+
def delete(self, workspace_slug: str, type_id: str) -> None:
66+
"""Delete a workspace work item type by ID.
67+
68+
Args:
69+
workspace_slug: The workspace slug identifier
70+
type_id: UUID of the work item type
71+
"""
72+
return self._delete(f"{workspace_slug}/work-item-types/{type_id}/")

plane/api/workspace_work_item_types/properties.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ class WorkspaceWorkItemTypeProperties(BaseResource):
1111
def __init__(self, config: Any) -> None:
1212
super().__init__(config, "/workspaces/")
1313

14-
def list(self, workspace_slug: str, type_id: str) -> list[WorkItemProperty]:
15-
"""List properties linked to a workspace work item type.
14+
def list(self, workspace_slug: str, type_id: str) -> list[str]:
15+
"""List property UUIDs linked to a workspace work item type.
16+
17+
The API returns a flat list of UUID strings, not full property objects.
18+
To get full WorkItemProperty objects, resolve these UUIDs via
19+
workspace_work_item_properties.list() or .retrieve().
1620
1721
Args:
1822
workspace_slug: The workspace slug identifier
@@ -21,7 +25,7 @@ def list(self, workspace_slug: str, type_id: str) -> list[WorkItemProperty]:
2125
response = self._get(
2226
f"{workspace_slug}/work-item-types/{type_id}/properties/"
2327
)
24-
return [WorkItemProperty.model_validate(item) for item in response]
28+
return list(response)
2529

2630
def create(
2731
self, workspace_slug: str, type_id: str, data: WorkspaceWorkItemTypePropertyLink

plane/models/work_items.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class WorkItem(BaseModel):
2323
updated_at: str | None = None
2424
deleted_at: str | None = None
2525
point: int | None = None
26-
name: str
26+
name: str | None = None
2727
description_html: str | None = None
2828
description_stripped: str | None = None
2929
description_binary: str | None = None
@@ -39,12 +39,12 @@ class WorkItem(BaseModel):
3939
external_id: str | None = None
4040
created_by: str | None = None
4141
updated_by: str | None = None
42-
project: str | None = None
42+
project: str | dict | None = None
4343
workspace: str | None = None
4444
parent: str | None = None
4545
state: str | StateLite | None = None
4646
estimate_point: str | None = None
47-
type: str | None = None
47+
type: str | dict | None = None
4848

4949

5050
class WorkItemDetail(BaseModel):
@@ -53,14 +53,14 @@ class WorkItemDetail(BaseModel):
5353
model_config = ConfigDict(extra="allow", populate_by_name=True)
5454

5555
id: str | None = None
56-
assignees: list[UserLite]
57-
labels: list[Label]
56+
assignees: list[UserLite] = Field(default_factory=list)
57+
labels: list[Label] = Field(default_factory=list)
5858
type_id: str | None = None
5959
created_at: str | None = None
6060
updated_at: str | None = None
6161
deleted_at: str | None = None
6262
point: int | None = None
63-
name: str
63+
name: str | None = None
6464
description_html: str | None = None
6565
description_stripped: str | None = None
6666
description_binary: str | None = None
@@ -76,12 +76,12 @@ class WorkItemDetail(BaseModel):
7676
external_id: str | None = None
7777
created_by: str | None = None
7878
updated_by: str | None = None
79-
project: str | None = None
79+
project: str | dict | None = None
8080
workspace: str | None = None
8181
parent: str | None = None
8282
state: str | StateLite | None = None
8383
estimate_point: str | None = None
84-
type: str | None = None
84+
type: str | dict | None = None
8585

8686

8787
class WorkItemExpand(BaseModel):
@@ -99,7 +99,7 @@ class WorkItemExpand(BaseModel):
9999
updated_at: str | None = None
100100
deleted_at: str | None = None
101101
point: int | None = None
102-
name: str
102+
name: str | None = None
103103
description: Any | None = None
104104
description_html: str | None = None
105105
description_stripped: str | None = None
@@ -116,11 +116,11 @@ class WorkItemExpand(BaseModel):
116116
external_id: str | None = None
117117
created_by: str | None = None
118118
updated_by: str | None = None
119-
project: str | None = None
119+
project: str | dict | None = None
120120
workspace: str | None = None
121121
parent: str | None = None
122122
estimate_point: str | None = None
123-
type: str | None = None
123+
type: str | dict | None = None
124124

125125

126126
class CreateWorkItem(BaseModel):

0 commit comments

Comments
 (0)