Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions plane/api/releases/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from ...models.releases import CreateRelease, Release, UpdateRelease
from ...models.releases import (
CreateRelease,
PaginatedReleaseResponse,
Release,
UpdateRelease,
)
from ..base_resource import BaseResource
from .comments import ReleaseComments
from .item_labels import ReleaseItemLabels
from .labels import ReleaseLabels
from .links import ReleaseLinks
from .tags import ReleaseTags
from .work_items import ReleaseWorkItems


class Releases(BaseResource):
Expand All @@ -17,15 +28,34 @@ def __init__(self, config: Any) -> None:
self.tags = ReleaseTags(config)
self.labels = ReleaseLabels(config)
self.item_labels = ReleaseItemLabels(config)
self.work_items = ReleaseWorkItems(config)
self.comments = ReleaseComments(config)
self.links = ReleaseLinks(config)

def list(
self, workspace_slug: str, params: Mapping[str, Any] | None = None
) -> PaginatedReleaseResponse:
"""List releases in the workspace (paginated).

def list(self, workspace_slug: str) -> list[Release]:
"""List all releases in the workspace.
Returns one page (20 by default). Pass `per_page`/`cursor` in params and
follow `next_cursor` to page through the rest.

Args:
workspace_slug: The workspace slug identifier
params: Optional query parameters, e.g. `per_page`, `cursor`
"""
response = self._get(f"{workspace_slug}/releases/")
return [Release.model_validate(item) for item in response]
response = self._get(f"{workspace_slug}/releases/", params=params)
return PaginatedReleaseResponse.model_validate(response)
Comment on lines +35 to +48

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Use typed Pydantic models for pagination parameters.

The new list methods expose raw Mapping[str, Any] inputs, bypassing validation and the repository’s DTO contract.

  • plane/api/releases/base.py#L35-L48: accept and serialize a release-list query model.
  • plane/api/releases/tags.py#L21-L34: accept and serialize a tag-list query model.
  • plane/api/releases/labels.py#L21-L34: accept and serialize a label-list query model.
  • plane/api/releases/item_labels.py#L20-L34: accept and serialize an item-label query model.
  • plane/api/releases/work_items.py#L19-L33: accept and serialize a work-item query model.
  • plane/api/releases/comments.py#L21-L35: accept and serialize a comment query model.
  • plane/api/releases/links.py#L21-L35: accept and serialize a link query model.

As per coding guidelines, resource methods must accept Pydantic DTOs and serialize them with model_dump(exclude_none=True).

📍 Affects 7 files
  • plane/api/releases/base.py#L35-L48 (this comment)
  • plane/api/releases/tags.py#L21-L34
  • plane/api/releases/labels.py#L21-L34
  • plane/api/releases/item_labels.py#L20-L34
  • plane/api/releases/work_items.py#L19-L33
  • plane/api/releases/comments.py#L21-L35
  • plane/api/releases/links.py#L21-L35
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plane/api/releases/base.py` around lines 35 - 48, Update each list
method—Release.list in plane/api/releases/base.py (35-48), Tag.list in
plane/api/releases/tags.py (21-34), Label.list in plane/api/releases/labels.py
(21-34), ItemLabel.list in plane/api/releases/item_labels.py (20-34),
WorkItem.list in plane/api/releases/work_items.py (19-33), Comment.list in
plane/api/releases/comments.py (21-35), and Link.list in
plane/api/releases/links.py (21-35)—to accept its corresponding typed Pydantic
query DTO instead of Mapping[str, Any], and pass params serialized with
model_dump(exclude_none=True) to _get.

Source: Coding guidelines


def retrieve(self, workspace_slug: str, release_id: str) -> Release:
"""Retrieve a release by ID.

Args:
workspace_slug: The workspace slug identifier
release_id: UUID of the release
"""
response = self._get(f"{workspace_slug}/releases/{release_id}/")
return Release.model_validate(response)

def create(self, workspace_slug: str, data: CreateRelease) -> Release:
"""Create a new release in the workspace.
Expand Down Expand Up @@ -53,3 +83,12 @@ def update(self, workspace_slug: str, release_id: str, data: UpdateRelease) -> R
data.model_dump(exclude_none=True),
)
return Release.model_validate(response)

def delete(self, workspace_slug: str, release_id: str) -> None:
"""Delete a release by ID.

Args:
workspace_slug: The workspace slug identifier
release_id: UUID of the release
"""
return self._delete(f"{workspace_slug}/releases/{release_id}/")
89 changes: 89 additions & 0 deletions plane/api/releases/comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from ...models.releases import (
CreateReleaseComment,
PaginatedReleaseCommentResponse,
ReleaseComment,
UpdateReleaseComment,
)
from ..base_resource import BaseResource


class ReleaseComments(BaseResource):
"""API client for the comments on a release."""

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

def list(
self, workspace_slug: str, release_id: str, params: Mapping[str, Any] | None = None
) -> PaginatedReleaseCommentResponse:
"""List the comments on a release (paginated).

Returns one page (20 by default). Pass `per_page`/`cursor` in params and
follow `next_cursor` to page through the rest.

Args:
workspace_slug: The workspace slug identifier
release_id: UUID of the release
params: Optional query parameters, e.g. `per_page`, `cursor`
"""
response = self._get(f"{workspace_slug}/releases/{release_id}/comments/", params=params)
return PaginatedReleaseCommentResponse.model_validate(response)

def retrieve(self, workspace_slug: str, release_id: str, comment_id: str) -> ReleaseComment:
"""Retrieve a release comment by ID.

Args:
workspace_slug: The workspace slug identifier
release_id: UUID of the release
comment_id: UUID of the comment
"""
response = self._get(f"{workspace_slug}/releases/{release_id}/comments/{comment_id}/")
return ReleaseComment.model_validate(response)

def create(
self, workspace_slug: str, release_id: str, data: CreateReleaseComment
) -> ReleaseComment:
"""Create a comment on a release.

Args:
workspace_slug: The workspace slug identifier
release_id: UUID of the release
data: Comment data (body as `comment_html`)
"""
response = self._post(
f"{workspace_slug}/releases/{release_id}/comments/",
data.model_dump(exclude_none=True),
)
return ReleaseComment.model_validate(response)

def update(
self, workspace_slug: str, release_id: str, comment_id: str, data: UpdateReleaseComment
) -> ReleaseComment:
"""Update a release comment by ID.

Args:
workspace_slug: The workspace slug identifier
release_id: UUID of the release
comment_id: UUID of the comment
data: Updated comment data
"""
response = self._patch(
f"{workspace_slug}/releases/{release_id}/comments/{comment_id}/",
data.model_dump(exclude_none=True),
)
return ReleaseComment.model_validate(response)

def delete(self, workspace_slug: str, release_id: str, comment_id: str) -> None:
"""Delete a release comment by ID.

Args:
workspace_slug: The workspace slug identifier
release_id: UUID of the release
comment_id: UUID of the comment
"""
return self._delete(f"{workspace_slug}/releases/{release_id}/comments/{comment_id}/")
39 changes: 27 additions & 12 deletions plane/api/releases/item_labels.py
Original file line number Diff line number Diff line change
@@ -1,49 +1,64 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from ...models.releases import AddReleaseItemLabel, ReleaseLabel
from ...models.releases import (
AddReleaseItemLabel,
PaginatedReleaseLabelResponse,
ReleaseLabel,
RemoveReleaseItemLabel,
)
from ..base_resource import BaseResource


class ReleaseItemLabels(BaseResource):
"""API client for managing labels on a specific release."""
"""API client for managing the labels attached to a specific release."""

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

def list(self, workspace_slug: str, release_id: str) -> list[ReleaseLabel]:
"""List labels assigned to a release.
def list(
self, workspace_slug: str, release_id: str, params: Mapping[str, Any] | None = None
) -> PaginatedReleaseLabelResponse:
"""List labels attached to a release (paginated).

Returns one page (20 by default). Pass `per_page`/`cursor` in params and
follow `next_cursor` to page through the rest.

Args:
workspace_slug: The workspace slug identifier
release_id: UUID of the release
params: Optional query parameters, e.g. `per_page`, `cursor`
"""
response = self._get(f"{workspace_slug}/releases/{release_id}/labels/")
return [ReleaseLabel.model_validate(item) for item in response]
response = self._get(f"{workspace_slug}/releases/{release_id}/labels/", params=params)
return PaginatedReleaseLabelResponse.model_validate(response)

def create(
self, workspace_slug: str, release_id: str, data: AddReleaseItemLabel
) -> list[ReleaseLabel]:
"""Add labels to a release.
"""Attach labels to a release.

Args:
workspace_slug: The workspace slug identifier
release_id: UUID of the release
data: Label IDs to add
data: Label IDs to attach
"""
response = self._post(
f"{workspace_slug}/releases/{release_id}/labels/",
data.model_dump(exclude_none=True),
)
return [ReleaseLabel.model_validate(item) for item in response]

def delete(self, workspace_slug: str, release_id: str, label_id: str) -> None:
"""Remove a label from a release.
def delete(self, workspace_slug: str, release_id: str, data: RemoveReleaseItemLabel) -> None:
"""Detach labels from a release (the labels themselves are not deleted).

Args:
workspace_slug: The workspace slug identifier
release_id: UUID of the release
label_id: UUID of the label to remove
data: Label IDs to detach
"""
return self._delete(f"{workspace_slug}/releases/{release_id}/labels/{label_id}/")
return self._delete(
f"{workspace_slug}/releases/{release_id}/labels/",
data.model_dump(exclude_none=True),
)
62 changes: 56 additions & 6 deletions plane/api/releases/labels.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,47 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from ...models.releases import CreateReleaseLabel, ReleaseLabel
from ...models.releases import (
CreateReleaseLabel,
PaginatedReleaseLabelResponse,
ReleaseLabel,
UpdateReleaseLabel,
)
from ..base_resource import BaseResource


class ReleaseLabels(BaseResource):
"""API client for managing release labels."""
"""API client for managing workspace-level release labels."""

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

def list(self, workspace_slug: str) -> list[ReleaseLabel]:
"""List all release labels in the workspace.
def list(
self, workspace_slug: str, params: Mapping[str, Any] | None = None
) -> PaginatedReleaseLabelResponse:
"""List release labels in the workspace (paginated).

Returns one page (20 by default). Pass `per_page`/`cursor` in params and
follow `next_cursor` to page through the rest.

Args:
workspace_slug: The workspace slug identifier
params: Optional query parameters, e.g. `per_page`, `cursor`
"""
response = self._get(f"{workspace_slug}/releases/labels/")
return [ReleaseLabel.model_validate(item) for item in response]
response = self._get(f"{workspace_slug}/releases/labels/", params=params)
return PaginatedReleaseLabelResponse.model_validate(response)

def retrieve(self, workspace_slug: str, label_id: str) -> ReleaseLabel:
"""Retrieve a release label by ID.

Args:
workspace_slug: The workspace slug identifier
label_id: UUID of the release label
"""
response = self._get(f"{workspace_slug}/releases/labels/{label_id}/")
return ReleaseLabel.model_validate(response)

def create(self, workspace_slug: str, data: CreateReleaseLabel) -> ReleaseLabel:
"""Create a new release label in the workspace.
Expand All @@ -31,3 +55,29 @@ def create(self, workspace_slug: str, data: CreateReleaseLabel) -> ReleaseLabel:
data.model_dump(exclude_none=True),
)
return ReleaseLabel.model_validate(response)

def update(self, workspace_slug: str, label_id: str, data: UpdateReleaseLabel) -> ReleaseLabel:
"""Update a release label by ID.

Args:
workspace_slug: The workspace slug identifier
label_id: UUID of the release label
data: Updated label data
"""
response = self._patch(
f"{workspace_slug}/releases/labels/{label_id}/",
data.model_dump(exclude_none=True),
)
return ReleaseLabel.model_validate(response)

def delete(self, workspace_slug: str, label_id: str) -> None:
"""Delete a release label by ID.

This deletes the label itself from the workspace. To only detach a label
from a release, use `client.releases.item_labels.delete`.

Args:
workspace_slug: The workspace slug identifier
label_id: UUID of the release label
"""
return self._delete(f"{workspace_slug}/releases/labels/{label_id}/")
Loading