Skip to content

Commit ac86f80

Browse files
[SILO-690] feat: new api clients and tests (#9)
* add clients for project ws features + stickies + initiatives + teamspaces * bump version to 0.2.1 --------- Co-authored-by: Surya Prashanth <[email protected]>
1 parent 81c96db commit ac86f80

31 files changed

+1664
-7
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ coverage.xml
4646
.hypothesis/
4747
venv/
4848
.venv/
49+
.env
4950
.python-version
5051
.pytest_cache
5152

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ client.epics # Epic management
234234
client.intake # Intake management
235235
client.pages # Page management
236236
client.customers # Customer management
237+
client.teamspaces # Teamspace management
238+
client.stickies # Sticky management
239+
client.initiatives # Initiative management
237240
```
238241

239242
### Resource Organization

plane/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from .api.cycles import Cycles
2+
from .api.initiatives import Initiatives
23
from .api.labels import Labels
34
from .api.modules import Modules
45
from .api.pages import Pages
56
from .api.projects import Projects
67
from .api.states import States
8+
from .api.stickies import Stickies
9+
from .api.teamspaces import Teamspaces
710
from .api.users import Users
811
from .api.work_item_properties import WorkItemProperties
912
from .api.work_item_types import WorkItemTypes
1013
from .api.work_items import WorkItems
14+
from .api.workspaces import Workspaces
1115
from .client import (
1216
OAuthAuthorizationParams,
1317
OAuthClient,
@@ -30,10 +34,14 @@
3034
"Projects",
3135
"Labels",
3236
"States",
37+
"Stickies",
38+
"Initiatives",
39+
"Teamspaces",
3340
"Users",
3441
"Modules",
3542
"Cycles",
3643
"Pages",
44+
"Workspaces",
3745
"PlaneError",
3846
"ConfigurationError",
3947
"HttpError",

plane/api/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from .base_resource import BaseResource
22
from .customers import Customers
3+
from .initiatives import Initiatives
4+
from .stickies import Stickies
5+
from .teamspaces import Teamspaces
36
from .work_item_properties import WorkItemProperties
47
from .work_items import WorkItems
58

@@ -8,4 +11,7 @@
811
"WorkItems",
912
"WorkItemProperties",
1013
"Customers",
14+
"Stickies",
15+
"Initiatives",
16+
"Teamspaces",
1117
]

plane/api/base_resource.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,11 @@ def _patch(self, endpoint: str, data: Mapping[str, Any] | None = None) -> Any:
5757
)
5858
return self._handle_response(response)
5959

60-
def _delete(self, endpoint: str) -> None:
60+
def _delete(self, endpoint: str, data: Mapping[str, Any] | None = None) -> None:
6161
url = self._build_url(endpoint)
62-
response = self.session.delete(url, headers=self._headers(), timeout=self.config.timeout)
62+
response = self.session.delete(
63+
url, headers=self._headers(), json=data, timeout=self.config.timeout
64+
)
6365
self._handle_response(response)
6466

6567
# Helpers

plane/api/initiatives/__init__.py

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

plane/api/initiatives/base.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from collections.abc import Mapping
2+
from typing import Any
3+
4+
from ...models.initiatives import (
5+
CreateInitiative,
6+
Initiative,
7+
PaginatedInitiativeResponse,
8+
UpdateInitiative,
9+
)
10+
from ..base_resource import BaseResource
11+
from .epics import InitiativeEpics
12+
from .labels import InitiativeLabels
13+
from .projects import InitiativeProjects
14+
15+
16+
class Initiatives(BaseResource):
17+
"""API client for managing initiatives in workspaces."""
18+
19+
def __init__(self, config: Any) -> None:
20+
super().__init__(config, "/workspaces/")
21+
22+
# Initialize sub-resources
23+
self.labels = InitiativeLabels(config)
24+
self.projects = InitiativeProjects(config)
25+
self.epics = InitiativeEpics(config)
26+
27+
def create(self, workspace_slug: str, data: CreateInitiative) -> Initiative:
28+
"""Create a new initiative in the workspace.
29+
30+
Args:
31+
workspace_slug: The workspace slug identifier
32+
data: Initiative data
33+
34+
Returns:
35+
The created initiative
36+
"""
37+
response = self._post(
38+
f"{workspace_slug}/initiatives",
39+
data.model_dump(exclude_none=True),
40+
)
41+
return Initiative.model_validate(response)
42+
43+
def retrieve(self, workspace_slug: str, initiative_id: str) -> Initiative:
44+
"""Retrieve an initiative by ID.
45+
46+
Args:
47+
workspace_slug: The workspace slug identifier
48+
initiative_id: UUID of the initiative
49+
50+
Returns:
51+
The requested initiative
52+
"""
53+
response = self._get(f"{workspace_slug}/initiatives/{initiative_id}")
54+
return Initiative.model_validate(response)
55+
56+
def update(
57+
self, workspace_slug: str, initiative_id: str, data: UpdateInitiative
58+
) -> Initiative:
59+
"""Update an initiative by ID.
60+
61+
Args:
62+
workspace_slug: The workspace slug identifier
63+
initiative_id: UUID of the initiative
64+
data: Updated initiative data
65+
66+
Returns:
67+
The updated initiative
68+
"""
69+
response = self._patch(
70+
f"{workspace_slug}/initiatives/{initiative_id}",
71+
data.model_dump(exclude_none=True),
72+
)
73+
return Initiative.model_validate(response)
74+
75+
def delete(self, workspace_slug: str, initiative_id: str) -> None:
76+
"""Delete an initiative by ID.
77+
78+
Args:
79+
workspace_slug: The workspace slug identifier
80+
initiative_id: UUID of the initiative
81+
"""
82+
return self._delete(f"{workspace_slug}/initiatives/{initiative_id}")
83+
84+
def list(
85+
self, workspace_slug: str, params: Mapping[str, Any] | None = None
86+
) -> PaginatedInitiativeResponse:
87+
"""List initiatives in the workspace with optional filtering.
88+
89+
Args:
90+
workspace_slug: The workspace slug identifier
91+
params: Optional query parameters (e.g., per_page, cursor)
92+
93+
Returns:
94+
Paginated list of initiatives
95+
"""
96+
response = self._get(f"{workspace_slug}/initiatives", params=params)
97+
return PaginatedInitiativeResponse.model_validate(response)
98+

plane/api/initiatives/epics.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from collections.abc import Iterable, Mapping
2+
from typing import Any
3+
4+
from ...models.epics import Epic, PaginatedEpicResponse
5+
from ..base_resource import BaseResource
6+
7+
8+
class InitiativeEpics(BaseResource):
9+
"""API client for managing epics associated with initiatives."""
10+
11+
def __init__(self, config: Any) -> None:
12+
super().__init__(config, "/workspaces/")
13+
14+
def list(
15+
self, workspace_slug: str, initiative_id: str, params: Mapping[str, Any] | None = None
16+
) -> PaginatedEpicResponse:
17+
"""List epics associated with an initiative.
18+
19+
Args:
20+
workspace_slug: The workspace slug identifier
21+
initiative_id: UUID of the initiative
22+
params: Optional query parameters (e.g., per_page, cursor)
23+
24+
Returns:
25+
Paginated list of epics
26+
"""
27+
response = self._get(f"{workspace_slug}/initiatives/{initiative_id}/epics", params=params)
28+
return PaginatedEpicResponse.model_validate(response)
29+
30+
def add(
31+
self, workspace_slug: str, initiative_id: str, epic_ids: Iterable[str]
32+
) -> Iterable[Epic]:
33+
"""Add epics to an initiative.
34+
35+
Args:
36+
workspace_slug: The workspace slug identifier
37+
initiative_id: UUID of the initiative
38+
epic_ids: List of epic UUIDs to add
39+
40+
Returns:
41+
List of added epics
42+
"""
43+
response = self._post(
44+
f"{workspace_slug}/initiatives/{initiative_id}/epics",
45+
{"epic_ids": epic_ids},
46+
)
47+
return [Epic.model_validate(epic) for epic in response]
48+
49+
def remove(self, workspace_slug: str, initiative_id: str, epic_ids: Iterable[str]) -> None:
50+
"""Remove epics from an initiative.
51+
52+
Args:
53+
workspace_slug: The workspace slug identifier
54+
initiative_id: UUID of the initiative
55+
epic_ids: List of epic UUIDs to remove
56+
"""
57+
return self._delete(
58+
f"{workspace_slug}/initiatives/{initiative_id}/epics",
59+
{"epic_ids": epic_ids},
60+
)

plane/api/initiatives/labels.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from collections.abc import Iterable, Mapping
2+
from typing import Any
3+
4+
from ...models.initiatives import (
5+
CreateInitiativeLabel,
6+
InitiativeLabel,
7+
PaginatedInitiativeLabelResponse,
8+
UpdateInitiativeLabel,
9+
)
10+
from ..base_resource import BaseResource
11+
12+
13+
class InitiativeLabels(BaseResource):
14+
"""API client for managing labels associated with initiatives."""
15+
16+
def __init__(self, config: Any) -> None:
17+
super().__init__(config, "/workspaces/")
18+
19+
def create(self, workspace_slug: str, data: CreateInitiativeLabel) -> InitiativeLabel:
20+
"""Create a new initiative label in the workspace.
21+
22+
Args:
23+
workspace_slug: The workspace slug identifier
24+
data: Initiative label data
25+
26+
Returns:
27+
The created initiative label
28+
"""
29+
response = self._post(
30+
f"{workspace_slug}/initiatives/labels",
31+
data.model_dump(exclude_none=True),
32+
)
33+
return InitiativeLabel.model_validate(response)
34+
35+
def retrieve(self, workspace_slug: str, label_id: str) -> InitiativeLabel:
36+
"""Retrieve an initiative label by ID.
37+
38+
Args:
39+
workspace_slug: The workspace slug identifier
40+
label_id: UUID of the initiative label
41+
42+
Returns:
43+
The requested initiative label
44+
"""
45+
response = self._get(f"{workspace_slug}/initiatives/labels/{label_id}")
46+
return InitiativeLabel.model_validate(response)
47+
48+
def update(
49+
self, workspace_slug: str, label_id: str, data: UpdateInitiativeLabel
50+
) -> InitiativeLabel:
51+
"""Update an initiative label by ID.
52+
53+
Args:
54+
workspace_slug: The workspace slug identifier
55+
label_id: UUID of the initiative label
56+
data: Updated initiative label data
57+
58+
Returns:
59+
The updated initiative label
60+
"""
61+
response = self._patch(
62+
f"{workspace_slug}/initiatives/labels/{label_id}",
63+
data.model_dump(exclude_none=True),
64+
)
65+
return InitiativeLabel.model_validate(response)
66+
67+
def delete(self, workspace_slug: str, label_id: str) -> None:
68+
"""Delete an initiative label by ID.
69+
70+
Args:
71+
workspace_slug: The workspace slug identifier
72+
label_id: UUID of the initiative label
73+
"""
74+
return self._delete(f"{workspace_slug}/initiatives/labels/{label_id}")
75+
76+
def list(
77+
self, workspace_slug: str, params: Mapping[str, Any] | None = None
78+
) -> PaginatedInitiativeLabelResponse:
79+
"""List initiative labels in the workspace with optional filtering.
80+
81+
Args:
82+
workspace_slug: The workspace slug identifier
83+
params: Optional query parameters (e.g., per_page, cursor)
84+
85+
Returns:
86+
Paginated list of initiative labels
87+
"""
88+
response = self._get(f"{workspace_slug}/initiatives/labels", params=params)
89+
return PaginatedInitiativeLabelResponse.model_validate(response)
90+
91+
def list_labels(
92+
self, workspace_slug: str, initiative_id: str, params: Mapping[str, Any] | None = None
93+
) -> PaginatedInitiativeLabelResponse:
94+
"""List labels associated with an initiative.
95+
96+
Args:
97+
workspace_slug: The workspace slug identifier
98+
initiative_id: UUID of the initiative
99+
params: Optional query parameters (e.g., per_page, cursor)
100+
101+
Returns:
102+
Paginated list of initiative labels
103+
"""
104+
response = self._get(f"{workspace_slug}/initiatives/{initiative_id}/labels", params=params)
105+
return PaginatedInitiativeLabelResponse.model_validate(response)
106+
107+
def add_labels(
108+
self, workspace_slug: str, initiative_id: str, label_ids: Iterable[str]
109+
) -> Iterable[InitiativeLabel]:
110+
"""Add labels to an initiative.
111+
112+
Args:
113+
workspace_slug: The workspace slug identifier
114+
initiative_id: UUID of the initiative
115+
label_ids: List of label UUIDs to add
116+
117+
Returns:
118+
List of added initiative labels
119+
"""
120+
response = self._post(
121+
f"{workspace_slug}/initiatives/{initiative_id}/labels",
122+
{"label_ids": label_ids},
123+
)
124+
return [InitiativeLabel.model_validate(label) for label in response]
125+
126+
def remove_labels(
127+
self, workspace_slug: str, initiative_id: str, label_ids: Iterable[str]
128+
) -> None:
129+
"""Remove labels from an initiative.
130+
131+
Args:
132+
workspace_slug: The workspace slug identifier
133+
initiative_id: UUID of the initiative
134+
label_ids: List of label UUIDs to remove
135+
"""
136+
return self._delete(
137+
f"{workspace_slug}/initiatives/{initiative_id}/labels",
138+
{"label_ids": label_ids},
139+
)

0 commit comments

Comments
 (0)