Skip to content

Commit dfcd8ec

Browse files
feat: enhance Releases API (#61)
* chore: enhance Releases API with pagination support * refactor: update work items and release tests to use paginated response structure * refactor: update ReleaseItemLabels and ReleaseWorkItems to use new request models for deletion * feat: add ReleaseChangelog model and integrate changelog functionality into Releases API
1 parent 5d9be38 commit dfcd8ec

10 files changed

Lines changed: 1054 additions & 127 deletions

File tree

plane/api/releases/base.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Mapping
14
from typing import Any
25

3-
from ...models.releases import CreateRelease, Release, UpdateRelease
6+
from ...models.releases import (
7+
CreateRelease,
8+
PaginatedReleaseResponse,
9+
Release,
10+
UpdateRelease,
11+
)
412
from ..base_resource import BaseResource
13+
from .changelog import ReleaseChangelogs
14+
from .comments import ReleaseComments
515
from .item_labels import ReleaseItemLabels
616
from .labels import ReleaseLabels
17+
from .links import ReleaseLinks
718
from .tags import ReleaseTags
19+
from .work_items import ReleaseWorkItems
820

921

1022
class Releases(BaseResource):
@@ -17,15 +29,35 @@ def __init__(self, config: Any) -> None:
1729
self.tags = ReleaseTags(config)
1830
self.labels = ReleaseLabels(config)
1931
self.item_labels = ReleaseItemLabels(config)
32+
self.work_items = ReleaseWorkItems(config)
33+
self.comments = ReleaseComments(config)
34+
self.links = ReleaseLinks(config)
35+
self.changelog = ReleaseChangelogs(config)
36+
37+
def list(
38+
self, workspace_slug: str, params: Mapping[str, Any] | None = None
39+
) -> PaginatedReleaseResponse:
40+
"""List releases in the workspace (paginated).
2041
21-
def list(self, workspace_slug: str) -> list[Release]:
22-
"""List all releases in the workspace.
42+
Returns one page (20 by default). Pass `per_page`/`cursor` in params and
43+
follow `next_cursor` to page through the rest.
2344
2445
Args:
2546
workspace_slug: The workspace slug identifier
47+
params: Optional query parameters, e.g. `per_page`, `cursor`
2648
"""
27-
response = self._get(f"{workspace_slug}/releases/")
28-
return [Release.model_validate(item) for item in response]
49+
response = self._get(f"{workspace_slug}/releases/", params=params)
50+
return PaginatedReleaseResponse.model_validate(response)
51+
52+
def retrieve(self, workspace_slug: str, release_id: str) -> Release:
53+
"""Retrieve a release by ID.
54+
55+
Args:
56+
workspace_slug: The workspace slug identifier
57+
release_id: UUID of the release
58+
"""
59+
response = self._get(f"{workspace_slug}/releases/{release_id}/")
60+
return Release.model_validate(response)
2961

3062
def create(self, workspace_slug: str, data: CreateRelease) -> Release:
3163
"""Create a new release in the workspace.
@@ -53,3 +85,12 @@ def update(self, workspace_slug: str, release_id: str, data: UpdateRelease) -> R
5385
data.model_dump(exclude_none=True),
5486
)
5587
return Release.model_validate(response)
88+
89+
def delete(self, workspace_slug: str, release_id: str) -> None:
90+
"""Delete a release by ID.
91+
92+
Args:
93+
workspace_slug: The workspace slug identifier
94+
release_id: UUID of the release
95+
"""
96+
return self._delete(f"{workspace_slug}/releases/{release_id}/")

plane/api/releases/changelog.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from ...models.releases import ReleaseChangelog, UpdateReleaseChangelog
6+
from ..base_resource import BaseResource
7+
8+
9+
class ReleaseChangelogs(BaseResource):
10+
"""API client for a release's changelog (a singleton per release)."""
11+
12+
def __init__(self, config: Any) -> None:
13+
super().__init__(config, "/workspaces/")
14+
15+
def retrieve(self, workspace_slug: str, release_id: str) -> ReleaseChangelog:
16+
"""Retrieve a release's changelog.
17+
18+
The changelog is created empty on first access, so this always returns one.
19+
20+
Args:
21+
workspace_slug: The workspace slug identifier
22+
release_id: UUID of the release
23+
"""
24+
response = self._get(f"{workspace_slug}/releases/{release_id}/changelog/")
25+
return ReleaseChangelog.model_validate(response)
26+
27+
def update(
28+
self, workspace_slug: str, release_id: str, data: UpdateReleaseChangelog
29+
) -> ReleaseChangelog:
30+
"""Update a release's changelog body.
31+
32+
Args:
33+
workspace_slug: The workspace slug identifier
34+
release_id: UUID of the release
35+
data: Updated changelog body (as `description_html` / `description_json`)
36+
"""
37+
response = self._patch(
38+
f"{workspace_slug}/releases/{release_id}/changelog/",
39+
data.model_dump(exclude_none=True),
40+
)
41+
return ReleaseChangelog.model_validate(response)

plane/api/releases/comments.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Mapping
4+
from typing import Any
5+
6+
from ...models.releases import (
7+
CreateReleaseComment,
8+
PaginatedReleaseCommentResponse,
9+
ReleaseComment,
10+
UpdateReleaseComment,
11+
)
12+
from ..base_resource import BaseResource
13+
14+
15+
class ReleaseComments(BaseResource):
16+
"""API client for the comments on a release."""
17+
18+
def __init__(self, config: Any) -> None:
19+
super().__init__(config, "/workspaces/")
20+
21+
def list(
22+
self, workspace_slug: str, release_id: str, params: Mapping[str, Any] | None = None
23+
) -> PaginatedReleaseCommentResponse:
24+
"""List the comments on a release (paginated).
25+
26+
Returns one page (20 by default). Pass `per_page`/`cursor` in params and
27+
follow `next_cursor` to page through the rest.
28+
29+
Args:
30+
workspace_slug: The workspace slug identifier
31+
release_id: UUID of the release
32+
params: Optional query parameters, e.g. `per_page`, `cursor`
33+
"""
34+
response = self._get(f"{workspace_slug}/releases/{release_id}/comments/", params=params)
35+
return PaginatedReleaseCommentResponse.model_validate(response)
36+
37+
def retrieve(self, workspace_slug: str, release_id: str, comment_id: str) -> ReleaseComment:
38+
"""Retrieve a release comment by ID.
39+
40+
Args:
41+
workspace_slug: The workspace slug identifier
42+
release_id: UUID of the release
43+
comment_id: UUID of the comment
44+
"""
45+
response = self._get(f"{workspace_slug}/releases/{release_id}/comments/{comment_id}/")
46+
return ReleaseComment.model_validate(response)
47+
48+
def create(
49+
self, workspace_slug: str, release_id: str, data: CreateReleaseComment
50+
) -> ReleaseComment:
51+
"""Create a comment on a release.
52+
53+
Args:
54+
workspace_slug: The workspace slug identifier
55+
release_id: UUID of the release
56+
data: Comment data (body as `comment_html`)
57+
"""
58+
response = self._post(
59+
f"{workspace_slug}/releases/{release_id}/comments/",
60+
data.model_dump(exclude_none=True),
61+
)
62+
return ReleaseComment.model_validate(response)
63+
64+
def update(
65+
self, workspace_slug: str, release_id: str, comment_id: str, data: UpdateReleaseComment
66+
) -> ReleaseComment:
67+
"""Update a release comment by ID.
68+
69+
Args:
70+
workspace_slug: The workspace slug identifier
71+
release_id: UUID of the release
72+
comment_id: UUID of the comment
73+
data: Updated comment data
74+
"""
75+
response = self._patch(
76+
f"{workspace_slug}/releases/{release_id}/comments/{comment_id}/",
77+
data.model_dump(exclude_none=True),
78+
)
79+
return ReleaseComment.model_validate(response)
80+
81+
def delete(self, workspace_slug: str, release_id: str, comment_id: str) -> None:
82+
"""Delete a release comment by ID.
83+
84+
Args:
85+
workspace_slug: The workspace slug identifier
86+
release_id: UUID of the release
87+
comment_id: UUID of the comment
88+
"""
89+
return self._delete(f"{workspace_slug}/releases/{release_id}/comments/{comment_id}/")

plane/api/releases/item_labels.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,64 @@
11
from __future__ import annotations
22

3+
from collections.abc import Mapping
34
from typing import Any
45

5-
from ...models.releases import AddReleaseItemLabel, ReleaseLabel
6+
from ...models.releases import (
7+
AddReleaseItemLabel,
8+
PaginatedReleaseLabelResponse,
9+
ReleaseLabel,
10+
RemoveReleaseItemLabel,
11+
)
612
from ..base_resource import BaseResource
713

814

915
class ReleaseItemLabels(BaseResource):
10-
"""API client for managing labels on a specific release."""
16+
"""API client for managing the labels attached to a specific release."""
1117

1218
def __init__(self, config: Any) -> None:
1319
super().__init__(config, "/workspaces/")
1420

15-
def list(self, workspace_slug: str, release_id: str) -> list[ReleaseLabel]:
16-
"""List labels assigned to a release.
21+
def list(
22+
self, workspace_slug: str, release_id: str, params: Mapping[str, Any] | None = None
23+
) -> PaginatedReleaseLabelResponse:
24+
"""List labels attached to a release (paginated).
25+
26+
Returns one page (20 by default). Pass `per_page`/`cursor` in params and
27+
follow `next_cursor` to page through the rest.
1728
1829
Args:
1930
workspace_slug: The workspace slug identifier
2031
release_id: UUID of the release
32+
params: Optional query parameters, e.g. `per_page`, `cursor`
2133
"""
22-
response = self._get(f"{workspace_slug}/releases/{release_id}/labels/")
23-
return [ReleaseLabel.model_validate(item) for item in response]
34+
response = self._get(f"{workspace_slug}/releases/{release_id}/labels/", params=params)
35+
return PaginatedReleaseLabelResponse.model_validate(response)
2436

2537
def create(
2638
self, workspace_slug: str, release_id: str, data: AddReleaseItemLabel
2739
) -> list[ReleaseLabel]:
28-
"""Add labels to a release.
40+
"""Attach labels to a release.
2941
3042
Args:
3143
workspace_slug: The workspace slug identifier
3244
release_id: UUID of the release
33-
data: Label IDs to add
45+
data: Label IDs to attach
3446
"""
3547
response = self._post(
3648
f"{workspace_slug}/releases/{release_id}/labels/",
3749
data.model_dump(exclude_none=True),
3850
)
3951
return [ReleaseLabel.model_validate(item) for item in response]
4052

41-
def delete(self, workspace_slug: str, release_id: str, label_id: str) -> None:
42-
"""Remove a label from a release.
53+
def delete(self, workspace_slug: str, release_id: str, data: RemoveReleaseItemLabel) -> None:
54+
"""Detach labels from a release (the labels themselves are not deleted).
4355
4456
Args:
4557
workspace_slug: The workspace slug identifier
4658
release_id: UUID of the release
47-
label_id: UUID of the label to remove
59+
data: Label IDs to detach
4860
"""
49-
return self._delete(f"{workspace_slug}/releases/{release_id}/labels/{label_id}/")
61+
return self._delete(
62+
f"{workspace_slug}/releases/{release_id}/labels/",
63+
data.model_dump(exclude_none=True),
64+
)

plane/api/releases/labels.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,47 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Mapping
14
from typing import Any
25

3-
from ...models.releases import CreateReleaseLabel, ReleaseLabel
6+
from ...models.releases import (
7+
CreateReleaseLabel,
8+
PaginatedReleaseLabelResponse,
9+
ReleaseLabel,
10+
UpdateReleaseLabel,
11+
)
412
from ..base_resource import BaseResource
513

614

715
class ReleaseLabels(BaseResource):
8-
"""API client for managing release labels."""
16+
"""API client for managing workspace-level release labels."""
917

1018
def __init__(self, config: Any) -> None:
1119
super().__init__(config, "/workspaces/")
1220

13-
def list(self, workspace_slug: str) -> list[ReleaseLabel]:
14-
"""List all release labels in the workspace.
21+
def list(
22+
self, workspace_slug: str, params: Mapping[str, Any] | None = None
23+
) -> PaginatedReleaseLabelResponse:
24+
"""List release labels in the workspace (paginated).
25+
26+
Returns one page (20 by default). Pass `per_page`/`cursor` in params and
27+
follow `next_cursor` to page through the rest.
1528
1629
Args:
1730
workspace_slug: The workspace slug identifier
31+
params: Optional query parameters, e.g. `per_page`, `cursor`
1832
"""
19-
response = self._get(f"{workspace_slug}/releases/labels/")
20-
return [ReleaseLabel.model_validate(item) for item in response]
33+
response = self._get(f"{workspace_slug}/releases/labels/", params=params)
34+
return PaginatedReleaseLabelResponse.model_validate(response)
35+
36+
def retrieve(self, workspace_slug: str, label_id: str) -> ReleaseLabel:
37+
"""Retrieve a release label by ID.
38+
39+
Args:
40+
workspace_slug: The workspace slug identifier
41+
label_id: UUID of the release label
42+
"""
43+
response = self._get(f"{workspace_slug}/releases/labels/{label_id}/")
44+
return ReleaseLabel.model_validate(response)
2145

2246
def create(self, workspace_slug: str, data: CreateReleaseLabel) -> ReleaseLabel:
2347
"""Create a new release label in the workspace.
@@ -31,3 +55,29 @@ def create(self, workspace_slug: str, data: CreateReleaseLabel) -> ReleaseLabel:
3155
data.model_dump(exclude_none=True),
3256
)
3357
return ReleaseLabel.model_validate(response)
58+
59+
def update(self, workspace_slug: str, label_id: str, data: UpdateReleaseLabel) -> ReleaseLabel:
60+
"""Update a release label by ID.
61+
62+
Args:
63+
workspace_slug: The workspace slug identifier
64+
label_id: UUID of the release label
65+
data: Updated label data
66+
"""
67+
response = self._patch(
68+
f"{workspace_slug}/releases/labels/{label_id}/",
69+
data.model_dump(exclude_none=True),
70+
)
71+
return ReleaseLabel.model_validate(response)
72+
73+
def delete(self, workspace_slug: str, label_id: str) -> None:
74+
"""Delete a release label by ID.
75+
76+
This deletes the label itself from the workspace. To only detach a label
77+
from a release, use `client.releases.item_labels.delete`.
78+
79+
Args:
80+
workspace_slug: The workspace slug identifier
81+
label_id: UUID of the release label
82+
"""
83+
return self._delete(f"{workspace_slug}/releases/labels/{label_id}/")

0 commit comments

Comments
 (0)