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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,6 @@ target/
.DS_Store
*.pem
.history
tsconfig.tsbuildinfo
tsconfig.tsbuildinfo

test.py
2 changes: 2 additions & 0 deletions plane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .api.cycles import Cycles
from .api.initiatives import Initiatives
from .api.labels import Labels
from .api.milestones import Milestones
from .api.modules import Modules
from .api.pages import Pages
from .api.projects import Projects
Expand Down Expand Up @@ -40,6 +41,7 @@
"Initiatives",
"Teamspaces",
"Users",
"Milestones",
"Modules",
"Cycles",
"Pages",
Expand Down
145 changes: 145 additions & 0 deletions plane/api/milestones.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from ..models.milestones import (
CreateMilestone,
Milestone,
MilestoneWorkItem,
PaginatedMilestoneResponse,
PaginatedMilestoneWorkItemResponse,
UpdateMilestone,
)
from .base_resource import BaseResource


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

def create(self, workspace_slug: str, project_id: str, data: CreateMilestone) -> Milestone:
"""Create a new milestone.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
data: Milestone data
"""
response = self._post(
f"{workspace_slug}/projects/{project_id}/milestones",
data.model_dump(exclude_none=True),
)
return Milestone.model_validate(response)

def retrieve(self, workspace_slug: str, project_id: str, milestone_id: str) -> Milestone:
"""Retrieve a milestone by ID.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
milestone_id: UUID of the milestone
"""
response = self._get(f"{workspace_slug}/projects/{project_id}/milestones/{milestone_id}")
return Milestone.model_validate(response)

def update(
self, workspace_slug: str, project_id: str, milestone_id: str, data: UpdateMilestone
) -> Milestone:
"""Update a milestone by ID.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
milestone_id: UUID of the milestone
data: Updated milestone data
"""
response = self._patch(
f"{workspace_slug}/projects/{project_id}/milestones/{milestone_id}",
data.model_dump(exclude_none=True),
)
return Milestone.model_validate(response)

def delete(self, workspace_slug: str, project_id: str, milestone_id: str) -> None:
"""Delete a milestone by ID.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
milestone_id: UUID of the milestone
"""
return self._delete(f"{workspace_slug}/projects/{project_id}/milestones/{milestone_id}")

def list(
self, workspace_slug: str, project_id: str, params: Mapping[str, Any] | None = None
) -> PaginatedMilestoneResponse:
"""List milestones with optional filtering parameters.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
params: Optional query parameters
"""
response = self._get(f"{workspace_slug}/projects/{project_id}/milestones", params=params)
return PaginatedMilestoneResponse.model_validate(response)

def add_work_items(
self,
workspace_slug: str,
project_id: str,
milestone_id: str,
issue_ids: list[str],
) -> None:
"""Add work items to a milestone.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
milestone_id: UUID of the milestone
issue_ids: List of issue IDs to add to the milestone
"""
return self._post(
f"{workspace_slug}/projects/{project_id}/milestones/{milestone_id}/work-items",
{"issues": issue_ids},
)

def remove_work_items(
self,
workspace_slug: str,
project_id: str,
milestone_id: str,
issue_ids: list[str],
) -> None:
"""Remove work items from a milestone.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
milestone_id: UUID of the milestone
issue_ids: List of issue IDs to remove from the milestone
"""
return self._delete(
f"{workspace_slug}/projects/{project_id}/milestones/{milestone_id}/work-items",
{"issues": issue_ids},
)

def list_work_items(
self,
workspace_slug: str,
project_id: str,
milestone_id: str,
params: Mapping[str, Any] | None = None,
) -> PaginatedMilestoneWorkItemResponse:
"""List work items in a milestone.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
milestone_id: UUID of the milestone
params: Optional query parameters
"""
response = self._get(
f"{workspace_slug}/projects/{project_id}/milestones/{milestone_id}/work-items",
params=params,
)
return PaginatedMilestoneWorkItemResponse.model_validate(response)
2 changes: 2 additions & 0 deletions plane/client/plane_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from ..api.initiatives import Initiatives
from ..api.intake import Intake
from ..api.labels import Labels
from ..api.milestones import Milestones
from ..api.modules import Modules
from ..api.pages import Pages
from ..api.projects import Projects
Expand Down Expand Up @@ -51,6 +52,7 @@ def __init__(
self.pages = Pages(self.config)
self.labels = Labels(self.config)
self.states = States(self.config)
self.milestones = Milestones(self.config)
self.modules = Modules(self.config)
self.cycles = Cycles(self.config)
self.work_item_types = WorkItemTypes(self.config)
Expand Down
84 changes: 84 additions & 0 deletions plane/models/milestones.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from typing import TYPE_CHECKING, Any

from pydantic import BaseModel, ConfigDict

from .pagination import PaginatedResponse

if TYPE_CHECKING:
from .work_items import WorkItem


class Milestone(BaseModel):
"""Milestone model."""

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

id: str | None = None
title: str
target_date: str | None = None
external_source: str | None = None
external_id: str | None = None
created_at: str | None = None
updated_at: str | None = None


class MilestoneLite(BaseModel):
"""Lite milestone information."""

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

id: str | None = None
title: str
target_date: str | None = None
external_source: str | None = None
external_id: str | None = None
created_at: str | None = None
updated_at: str | None = None


class CreateMilestone(BaseModel):
"""Request model for creating a milestone."""

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

title: str
target_date: str | None = None
external_source: str | None = None
external_id: str | None = None


class UpdateMilestone(BaseModel):
"""Request model for updating a milestone."""

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

title: str | None = None
target_date: str | None = None
external_source: str | None = None
external_id: str | None = None


class MilestoneWorkItem(BaseModel):
"""Work item in a milestone."""

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

id: str | None = None
issue: str | None = None
milestone: str | None = None


class PaginatedMilestoneResponse(PaginatedResponse):
"""Paginated response for milestones."""

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

results: list[Milestone]


class PaginatedMilestoneWorkItemResponse(PaginatedResponse):
"""Paginated response for milestone work items."""

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

results: list[MilestoneWorkItem]
Loading