Skip to content

Commit 6e5389f

Browse files
authored
api methods for estimates feature (#31)
* api methods for estimates feature * refactoring * change version * archive methods for workitems and projects
1 parent 97e0307 commit 6e5389f

11 files changed

Lines changed: 843 additions & 2 deletions

File tree

plane/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .api.agent_runs import AgentRuns
22
from .api.cycles import Cycles
3+
from .api.estimates import Estimates
34
from .api.initiatives import Initiatives
45
from .api.labels import Labels
56
from .api.milestones import Milestones
@@ -44,6 +45,7 @@
4445
"Milestones",
4546
"Modules",
4647
"Cycles",
48+
"Estimates",
4749
"Pages",
4850
"Workspaces",
4951
"PlaneError",

plane/api/base_resource.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ def _get(self, endpoint: str, params: Mapping[str, Any] | None = None) -> Any:
3636
)
3737
return self._handle_response(response)
3838

39-
def _post(self, endpoint: str, data: Mapping[str, Any] | None = None) -> Any:
39+
def _post(
40+
self, endpoint: str, data: Mapping[str, Any] | list[Any] | None = None
41+
) -> Any:
4042
url = self._build_url(endpoint)
4143
response = self.session.post(
4244
url, headers=self._headers(), json=data, timeout=self.config.timeout

plane/api/estimates.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from ..models.estimates import (
6+
CreateEstimate,
7+
CreateEstimatePoint,
8+
Estimate,
9+
EstimatePoint,
10+
UpdateEstimate,
11+
UpdateEstimatePoint,
12+
)
13+
from .base_resource import BaseResource
14+
15+
16+
class Estimates(BaseResource):
17+
"""Resource for managing project estimates and estimate points."""
18+
19+
def __init__(self, config: Any) -> None:
20+
super().__init__(config, "/workspaces/")
21+
22+
# ── Estimate CRUD ────────────────────────────────────────────
23+
24+
def create(
25+
self,
26+
workspace_slug: str,
27+
project_id: str,
28+
data: CreateEstimate,
29+
) -> Estimate:
30+
"""Create a new estimate for a project.
31+
32+
Args:
33+
workspace_slug: The workspace slug identifier.
34+
project_id: UUID of the project.
35+
data: Estimate creation data.
36+
"""
37+
response = self._post(
38+
f"{workspace_slug}/projects/{project_id}/estimates",
39+
data.model_dump(exclude_none=True),
40+
)
41+
return Estimate.model_validate(response)
42+
43+
def link_to_project(
44+
self,
45+
workspace_slug: str,
46+
project_id: str,
47+
estimate_id: str,
48+
) -> Any:
49+
"""Link an estimate to a project so that it becomes the active estimate system.
50+
51+
Args:
52+
workspace_slug: The workspace slug identifier.
53+
project_id: UUID of the project.
54+
estimate_id: UUID of the estimate to link.
55+
"""
56+
from ..models.projects import UpdateProject
57+
from .projects import Projects
58+
59+
projects_client = Projects(self.config)
60+
return projects_client.update(
61+
workspace_slug,
62+
project_id,
63+
UpdateProject(estimate=estimate_id),
64+
)
65+
66+
def retrieve(
67+
self,
68+
workspace_slug: str,
69+
project_id: str,
70+
) -> Estimate:
71+
"""Retrieve the estimate configured for a project.
72+
73+
Args:
74+
workspace_slug: The workspace slug identifier.
75+
project_id: UUID of the project.
76+
"""
77+
response = self._get(
78+
f"{workspace_slug}/projects/{project_id}/estimates",
79+
)
80+
return Estimate.model_validate(response)
81+
82+
def update(
83+
self,
84+
workspace_slug: str,
85+
project_id: str,
86+
data: UpdateEstimate,
87+
) -> Estimate:
88+
"""Update the estimate for a project.
89+
90+
Args:
91+
workspace_slug: The workspace slug identifier.
92+
project_id: UUID of the project.
93+
data: Fields to update.
94+
"""
95+
response = self._patch(
96+
f"{workspace_slug}/projects/{project_id}/estimates",
97+
data.model_dump(exclude_none=True),
98+
)
99+
return Estimate.model_validate(response)
100+
101+
def delete(
102+
self,
103+
workspace_slug: str,
104+
project_id: str,
105+
) -> None:
106+
"""Delete the estimate for a project.
107+
108+
Args:
109+
workspace_slug: The workspace slug identifier.
110+
project_id: UUID of the project.
111+
"""
112+
return self._delete(
113+
f"{workspace_slug}/projects/{project_id}/estimates",
114+
)
115+
116+
# ── Estimate Points ──────────────────────────────────────────
117+
118+
def list_points(
119+
self,
120+
workspace_slug: str,
121+
project_id: str,
122+
estimate_id: str,
123+
) -> list[EstimatePoint]:
124+
"""List all estimate points for a project estimate.
125+
126+
Args:
127+
workspace_slug: The workspace slug identifier.
128+
project_id: UUID of the project.
129+
estimate_id: UUID of the estimate.
130+
"""
131+
response = self._get(
132+
f"{workspace_slug}/projects/{project_id}"
133+
f"/estimates/{estimate_id}/estimate-points",
134+
)
135+
return [EstimatePoint.model_validate(item) for item in response]
136+
137+
def create_points(
138+
self,
139+
workspace_slug: str,
140+
project_id: str,
141+
estimate_id: str,
142+
data: list[CreateEstimatePoint],
143+
) -> list[EstimatePoint]:
144+
"""Create estimate points for a project estimate.
145+
146+
The API accepts a JSON array directly as the request body.
147+
148+
Args:
149+
workspace_slug: The workspace slug identifier.
150+
project_id: UUID of the project.
151+
estimate_id: UUID of the estimate.
152+
data: List of estimate point creation data.
153+
"""
154+
payload = [item.model_dump(exclude_none=True) for item in data]
155+
response = self._post(
156+
f"{workspace_slug}/projects/{project_id}"
157+
f"/estimates/{estimate_id}/estimate-points",
158+
payload,
159+
)
160+
return [EstimatePoint.model_validate(item) for item in response]
161+
162+
def update_point(
163+
self,
164+
workspace_slug: str,
165+
project_id: str,
166+
estimate_id: str,
167+
estimate_point_id: str,
168+
data: UpdateEstimatePoint,
169+
) -> EstimatePoint:
170+
"""Update a single estimate point.
171+
172+
Args:
173+
workspace_slug: The workspace slug identifier.
174+
project_id: UUID of the project.
175+
estimate_id: UUID of the estimate.
176+
estimate_point_id: UUID of the estimate point.
177+
data: Fields to update.
178+
"""
179+
response = self._patch(
180+
f"{workspace_slug}/projects/{project_id}"
181+
f"/estimates/{estimate_id}/estimate-points/{estimate_point_id}",
182+
data.model_dump(exclude_none=True),
183+
)
184+
return EstimatePoint.model_validate(response)
185+
186+
def delete_point(
187+
self,
188+
workspace_slug: str,
189+
project_id: str,
190+
estimate_id: str,
191+
estimate_point_id: str,
192+
) -> None:
193+
"""Delete a single estimate point.
194+
195+
Args:
196+
workspace_slug: The workspace slug identifier.
197+
project_id: UUID of the project.
198+
estimate_id: UUID of the estimate.
199+
estimate_point_id: UUID of the estimate point.
200+
"""
201+
return self._delete(
202+
f"{workspace_slug}/projects/{project_id}"
203+
f"/estimates/{estimate_id}/estimate-points/{estimate_point_id}",
204+
)

plane/api/projects.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,32 @@ def update_features(
120120
f"{workspace_slug}/projects/{project_id}/features", data.model_dump(exclude_none=True)
121121
)
122122
return ProjectFeature.model_validate(response)
123+
124+
def archive(self, workspace_slug: str, project_id: str) -> None:
125+
"""Archive a project.
126+
127+
Move a project to archived status, hiding it from active project lists.
128+
129+
Args:
130+
workspace_slug: The workspace slug identifier
131+
project_id: UUID of the project
132+
133+
Returns:
134+
None (HTTP 204 No Content)
135+
"""
136+
self._post(f"{workspace_slug}/projects/{project_id}/archive", {})
137+
138+
def unarchive(self, workspace_slug: str, project_id: str) -> None:
139+
"""Unarchive a project.
140+
141+
Restore an archived project to active status, making it available
142+
in regular workflows.
143+
144+
Args:
145+
workspace_slug: The workspace slug identifier
146+
project_id: UUID of the project
147+
148+
Returns:
149+
None (HTTP 204 No Content)
150+
"""
151+
self._delete(f"{workspace_slug}/projects/{project_id}/archive")

plane/api/work_items/base.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,35 @@ def advanced_search(
236236
data.model_dump(exclude_none=True),
237237
)
238238
return [AdvancedSearchResult.model_validate(item) for item in response]
239+
240+
def archive(self, workspace_slug: str, project_id: str, work_item_id: str) -> None:
241+
"""Archive a work item.
242+
243+
Only work items in a completed or cancelled state can be archived.
244+
245+
Args:
246+
workspace_slug: The workspace slug identifier
247+
project_id: UUID of the project
248+
work_item_id: UUID of the work item
249+
"""
250+
self._post(
251+
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/archive",
252+
{},
253+
)
254+
255+
def unarchive(self, workspace_slug: str, project_id: str, work_item_id: str) -> None:
256+
"""Unarchive a work item.
257+
258+
Restore an archived work item to active status.
259+
260+
Args:
261+
workspace_slug: The workspace slug identifier
262+
project_id: UUID of the project
263+
work_item_id: UUID of the work item
264+
265+
Returns:
266+
None (HTTP 204 No Content)
267+
"""
268+
self._delete(
269+
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/unarchive"
270+
)

plane/client/plane_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from ..api.customers import Customers
33
from ..api.cycles import Cycles
44
from ..api.epics import Epics
5+
from ..api.estimates import Estimates
56
from ..api.initiatives import Initiatives
67
from ..api.intake import Intake
78
from ..api.labels import Labels
@@ -55,6 +56,7 @@ def __init__(
5556
self.milestones = Milestones(self.config)
5657
self.modules = Modules(self.config)
5758
self.cycles = Cycles(self.config)
59+
self.estimates = Estimates(self.config)
5860
self.work_item_types = WorkItemTypes(self.config)
5961
self.work_item_properties = WorkItemProperties(self.config)
6062
self.customers = Customers(self.config)

0 commit comments

Comments
 (0)