Skip to content
Merged
15 changes: 12 additions & 3 deletions plane/api/work_item_relation_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from ..models.work_item_relation_definitions import (
CreateWorkItemRelationDefinition,
PaginatedWorkItemRelationDefinitionResponse,
UpdateWorkItemRelationDefinition,
WorkItemRelationDefinition,
)
Expand All @@ -19,25 +20,33 @@ def list(
workspace_slug: str,
is_default: bool | None = None,
is_active: bool | None = None,
) -> list[WorkItemRelationDefinition]:
"""List all work item relation definitions in the workspace.
per_page: int | None = None,
cursor: str | None = None,
) -> PaginatedWorkItemRelationDefinitionResponse:
"""List work item relation definitions in the workspace.

Args:
workspace_slug: The workspace slug identifier
is_default: Optional filter by default status
is_active: Optional filter by active status
per_page: Number of results per page (default 100)
cursor: Pagination cursor from a previous response's next_cursor
"""
params: dict[str, Any] = {}
if is_default is not None:
params["is_default"] = str(is_default).lower()
if is_active is not None:
params["is_active"] = str(is_active).lower()
if per_page is not None:
params["per_page"] = per_page
if cursor is not None:
params["cursor"] = cursor

response = self._get(
f"{workspace_slug}/work-item-relation-definitions/",
params=params if params else None,
)
return [WorkItemRelationDefinition.model_validate(item) for item in response]
return PaginatedWorkItemRelationDefinitionResponse.model_validate(response)

def create(
self, workspace_slug: str, data: CreateWorkItemRelationDefinition
Expand Down
6 changes: 5 additions & 1 deletion plane/api/work_items/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
from .activities import WorkItemActivities
from .attachments import WorkItemAttachments
from .comments import WorkItemComments
from .custom_relations import WorkItemCustomRelations
from .dependencies import WorkItemDependencies
from .links import WorkItemLinks
from .pages import WorkItemPages
from .relations import WorkItemRelations
Expand Down Expand Up @@ -76,6 +78,8 @@ def __init__(self, config: Any) -> None:

# Initialize sub-resources
self.relations = WorkItemRelations(config)
self.dependencies = WorkItemDependencies(config)
self.custom_relations = WorkItemCustomRelations(config)
self.links = WorkItemLinks(config)
self.attachments = WorkItemAttachments(config)
self.comments = WorkItemComments(config)
Expand Down Expand Up @@ -355,7 +359,7 @@ def search(
query: Search query string
params: Optional query parameters for expand, fields, etc.
"""
search_params = {"q": query}
search_params = {"search": query}
if params:
search_params.update(params.model_dump(exclude_none=True))
response = self._get(f"{workspace_slug}/work-items/search", params=search_params)
Expand Down
82 changes: 82 additions & 0 deletions plane/api/work_items/custom_relations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

from typing import Any

from ...models.work_items import (
CreateWorkItemCustomRelation,
WorkItemWithRelationType,
)
from ..base_resource import BaseResource


class WorkItemCustomRelations(BaseResource):
"""API client for managing custom (definition-based) work item relations.

Custom relations are workspace-level types defined via the
work-item-relation-definitions endpoint. Each definition has an outward label
and an inward label that controls directionality.
"""

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

def list(
self, workspace_slug: str, project_id: str, work_item_id: str
) -> dict[str, list[WorkItemWithRelationType]]:
"""List all custom relations for a work item grouped by definition label.

Response keys are the outward/inward labels from active workspace relation
definitions (e.g. 'implements', 'implemented by', 'relates to').

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
work_item_id: UUID of the work item
"""
response = self._get(
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/work-item-relations/"
)
return {
label: [WorkItemWithRelationType.model_validate(item) for item in items]
for label, items in response.items()
}

def create(
self,
workspace_slug: str,
project_id: str,
work_item_id: str,
data: CreateWorkItemCustomRelation,
) -> list[WorkItemWithRelationType]:
"""Create one or more custom relations for a work item.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
work_item_id: UUID of the work item
data: Custom relation creation payload
"""
response = self._post(
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/work-item-relations/",
data.model_dump(exclude_none=True),
)
return [WorkItemWithRelationType.model_validate(item) for item in response]

def remove(
self,
workspace_slug: str,
project_id: str,
work_item_id: str,
related_work_item_id: str,
) -> None:
"""Remove a custom relation between this work item and a target.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
work_item_id: UUID of the work item
related_work_item_id: UUID of the related work item to remove the relation with
"""
self._delete(
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/work-item-relations/{related_work_item_id}/"
)
76 changes: 76 additions & 0 deletions plane/api/work_items/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from __future__ import annotations

from typing import Any

from ...models.work_items import (
CreateWorkItemDependency,
WorkItemDependencyResponse,
WorkItemWithRelationType,
)
from ..base_resource import BaseResource


class WorkItemDependencies(BaseResource):
"""API client for managing work item dependency relations.

Covers the six built-in dependency directions:
blocking / blocked_by / start_before / start_after / finish_before / finish_after.
"""

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

def list(
self, workspace_slug: str, project_id: str, work_item_id: str
) -> WorkItemDependencyResponse:
"""List all dependency relations for a work item grouped by direction.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
work_item_id: UUID of the work item
"""
response = self._get(
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/dependencies/"
)
return WorkItemDependencyResponse.model_validate(response)

def create(
self,
workspace_slug: str,
project_id: str,
work_item_id: str,
data: CreateWorkItemDependency,
) -> list[WorkItemWithRelationType]:
"""Create one or more dependency relations for a work item.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
work_item_id: UUID of the work item
data: Dependency creation payload
"""
response = self._post(
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/dependencies/",
data.model_dump(exclude_none=True),
)
return [WorkItemWithRelationType.model_validate(item) for item in response]

def remove(
self,
workspace_slug: str,
project_id: str,
work_item_id: str,
related_work_item_id: str,
) -> None:
"""Remove a dependency relation between this work item and a target.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
work_item_id: UUID of the work item
related_work_item_id: UUID of the related work item to remove the dependency with
"""
self._delete(
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/dependencies/{related_work_item_id}/"
)
10 changes: 10 additions & 0 deletions plane/models/work_item_relation_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from pydantic import BaseModel, ConfigDict

from .pagination import PaginatedResponse


class WorkItemRelationDefinition(BaseModel):
"""Work item relation definition response model."""
Expand Down Expand Up @@ -47,3 +49,11 @@ class UpdateWorkItemRelationDefinition(BaseModel):
is_active: bool | None = None
color: str | None = None
sort_order: float | None = None


class PaginatedWorkItemRelationDefinitionResponse(PaginatedResponse):
"""Paginated response for work item relation definitions."""

model_config = ConfigDict(extra="allow", populate_by_name=True)

results: list[WorkItemRelationDefinition]
2 changes: 2 additions & 0 deletions plane/models/work_item_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class CreateWorkItemType(BaseModel):
logo_props: Any | None = None
is_epic: bool | None = None
is_active: bool | None = None
level: int | None = None
external_source: str | None = None
external_id: str | None = None

Expand All @@ -53,6 +54,7 @@ class UpdateWorkItemType(BaseModel):
logo_props: Any | None = None
is_epic: bool | None = None
is_active: bool | None = None
level: int | None = None
external_source: str | None = None
external_id: str | None = None

Expand Down
108 changes: 106 additions & 2 deletions plane/models/work_items.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal

from pydantic import BaseModel, ConfigDict, Field

Expand Down Expand Up @@ -201,7 +201,7 @@ class WorkItemSearchItem(BaseModel):

id: str = Field(..., description="Issue ID")
name: str = Field(..., description="Issue name")
sequence_id: str = Field(..., description="Issue sequence ID")
sequence_id: int = Field(..., description="Issue sequence ID")
project__identifier: str = Field(..., description="Project identifier")
project_id: str = Field(..., description="Project ID")
workspace__slug: str = Field(..., description="Workspace slug")
Expand Down Expand Up @@ -525,6 +525,110 @@ class WorkItemRelationResponse(BaseModel):
)


DependencyTypeEnum = Literal[
"blocking",
"blocked_by",
"start_before",
"start_after",
"finish_before",
"finish_after",
]


class WorkItemWithRelationType(BaseModel):
"""Work item with an injected relation_type label."""

model_config = ConfigDict(extra="allow", populate_by_name=True)

id: str | None = None
name: str | None = None
sequence_id: int | None = None
project_id: str | None = None
state_id: str | None = None
priority: str | None = None
type_id: str | None = None
is_epic: bool | None = None
label_ids: list[str] = Field(default_factory=list)
assignee_ids: list[str] = Field(default_factory=list)
sort_order: float | None = None
created_at: str | None = None
updated_at: str | None = None
created_by: str | None = None
updated_by: str | None = None
relation_type: str | None = None


class WorkItemDependencyResponse(BaseModel):
"""Response model for GET /relation-dependencies/."""

model_config = ConfigDict(extra="allow", populate_by_name=True)

blocking: list[WorkItemWithRelationType] = Field(default_factory=list)
blocked_by: list[WorkItemWithRelationType] = Field(default_factory=list)
start_before: list[WorkItemWithRelationType] = Field(default_factory=list)
start_after: list[WorkItemWithRelationType] = Field(default_factory=list)
finish_before: list[WorkItemWithRelationType] = Field(default_factory=list)
finish_after: list[WorkItemWithRelationType] = Field(default_factory=list)


class CreateWorkItemDependency(BaseModel):
"""Request model for creating work item dependency relations."""

model_config = ConfigDict(extra="ignore", populate_by_name=True)

relation_type: DependencyTypeEnum = Field(
...,
description="Dependency direction from the perspective of this work item",
)
work_item_ids: list[str] = Field(
...,
description="UUIDs of work items to create dependencies with",
min_length=1,
)


class RemoveWorkItemDependency(BaseModel):
"""Request model for removing a work item dependency."""

model_config = ConfigDict(extra="ignore", populate_by_name=True)

work_item_id: str = Field(
...,
description="UUID of the related work item whose dependency should be removed",
)


class CreateWorkItemCustomRelation(BaseModel):
"""Request model for creating a custom (definition-based) work item relation."""

model_config = ConfigDict(extra="ignore", populate_by_name=True)

relation_definition_id: str = Field(
...,
description="UUID of the workspace relation definition",
)
relation_definition_type: str = Field(
...,
description="The outward or inward label of the definition (controls directionality)",
)
work_item_ids: list[str] = Field(
...,
description="UUIDs of work items to create the relation with",
min_length=1,
)


class RemoveWorkItemCustomRelation(BaseModel):
"""Request model for removing a custom work item relation."""

model_config = ConfigDict(extra="ignore", populate_by_name=True)

work_item_id: str = Field(
...,
description="UUID of the related work item whose custom relation should be removed",
)


class WorkItemWorkLog(BaseModel):
"""Work item work log model."""

Expand Down
Loading