Skip to content
Draft
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
4 changes: 4 additions & 0 deletions 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
84 changes: 84 additions & 0 deletions plane/api/work_items/custom_relations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from __future__ import annotations

from typing import Any

from ...models.work_items import (
CreateWorkItemCustomRelation,
RemoveWorkItemCustomRelation,
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,
data: RemoveWorkItemCustomRelation,
) -> 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
data: Removal payload containing the related work item UUID
"""
return self._post(
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/work-item-relations/remove/",
data.model_dump(exclude_none=True),
)
78 changes: 78 additions & 0 deletions plane/api/work_items/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from __future__ import annotations

from typing import Any

from ...models.work_items import (
CreateWorkItemDependency,
RemoveWorkItemDependency,
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}/relation-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}/relation-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,
data: RemoveWorkItemDependency,
) -> 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
data: Removal payload containing the related work item UUID
"""
return self._post(
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/relation-dependencies/remove/",
data.model_dump(exclude_none=True),
)
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
106 changes: 105 additions & 1 deletion 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 @@ -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