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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ coverage.xml
.hypothesis/
venv/
.venv/
.env
.python-version
.pytest_cache

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ client.epics # Epic management
client.intake # Intake management
client.pages # Page management
client.customers # Customer management
client.teamspaces # Teamspace management
client.stickies # Sticky management
client.initiatives # Initiative management
```

### Resource Organization
Expand Down
8 changes: 8 additions & 0 deletions plane/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from .api.cycles import Cycles
from .api.initiatives import Initiatives
from .api.labels import Labels
from .api.modules import Modules
from .api.pages import Pages
from .api.projects import Projects
from .api.states import States
from .api.stickies import Stickies
from .api.teamspaces import Teamspaces
from .api.users import Users
from .api.work_item_properties import WorkItemProperties
from .api.work_item_types import WorkItemTypes
from .api.work_items import WorkItems
from .api.workspaces import Workspaces
from .client import (
OAuthAuthorizationParams,
OAuthClient,
Expand All @@ -30,10 +34,14 @@
"Projects",
"Labels",
"States",
"Stickies",
"Initiatives",
"Teamspaces",
"Users",
"Modules",
"Cycles",
"Pages",
"Workspaces",
"PlaneError",
"ConfigurationError",
"HttpError",
Expand Down
6 changes: 6 additions & 0 deletions plane/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from .base_resource import BaseResource
from .customers import Customers
from .initiatives import Initiatives
from .stickies import Stickies
from .teamspaces import Teamspaces
from .work_item_properties import WorkItemProperties
from .work_items import WorkItems

Expand All @@ -8,4 +11,7 @@
"WorkItems",
"WorkItemProperties",
"Customers",
"Stickies",
"Initiatives",
"Teamspaces",
]
6 changes: 4 additions & 2 deletions plane/api/base_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ def _patch(self, endpoint: str, data: Mapping[str, Any] | None = None) -> Any:
)
return self._handle_response(response)

def _delete(self, endpoint: str) -> None:
def _delete(self, endpoint: str, data: Mapping[str, Any] | None = None) -> None:
url = self._build_url(endpoint)
response = self.session.delete(url, headers=self._headers(), timeout=self.config.timeout)
response = self.session.delete(
url, headers=self._headers(), json=data, timeout=self.config.timeout
)
self._handle_response(response)

# Helpers
Expand Down
4 changes: 4 additions & 0 deletions plane/api/initiatives/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .base import Initiatives

__all__ = ["Initiatives"]

98 changes: 98 additions & 0 deletions plane/api/initiatives/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from collections.abc import Mapping
from typing import Any

from ...models.initiatives import (
CreateInitiative,
Initiative,
PaginatedInitiativeResponse,
UpdateInitiative,
)
from ..base_resource import BaseResource
from .epics import InitiativeEpics
from .labels import InitiativeLabels
from .projects import InitiativeProjects


class Initiatives(BaseResource):
"""API client for managing initiatives in workspaces."""

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

# Initialize sub-resources
self.labels = InitiativeLabels(config)
self.projects = InitiativeProjects(config)
self.epics = InitiativeEpics(config)

def create(self, workspace_slug: str, data: CreateInitiative) -> Initiative:
"""Create a new initiative in the workspace.

Args:
workspace_slug: The workspace slug identifier
data: Initiative data

Returns:
The created initiative
"""
response = self._post(
f"{workspace_slug}/initiatives",
data.model_dump(exclude_none=True),
)
return Initiative.model_validate(response)

def retrieve(self, workspace_slug: str, initiative_id: str) -> Initiative:
"""Retrieve an initiative by ID.

Args:
workspace_slug: The workspace slug identifier
initiative_id: UUID of the initiative

Returns:
The requested initiative
"""
response = self._get(f"{workspace_slug}/initiatives/{initiative_id}")
return Initiative.model_validate(response)

def update(
self, workspace_slug: str, initiative_id: str, data: UpdateInitiative
) -> Initiative:
"""Update an initiative by ID.

Args:
workspace_slug: The workspace slug identifier
initiative_id: UUID of the initiative
data: Updated initiative data

Returns:
The updated initiative
"""
response = self._patch(
f"{workspace_slug}/initiatives/{initiative_id}",
data.model_dump(exclude_none=True),
)
return Initiative.model_validate(response)

def delete(self, workspace_slug: str, initiative_id: str) -> None:
"""Delete an initiative by ID.

Args:
workspace_slug: The workspace slug identifier
initiative_id: UUID of the initiative
"""
return self._delete(f"{workspace_slug}/initiatives/{initiative_id}")

def list(
self, workspace_slug: str, params: Mapping[str, Any] | None = None
) -> PaginatedInitiativeResponse:
"""List initiatives in the workspace with optional filtering.

Args:
workspace_slug: The workspace slug identifier
params: Optional query parameters (e.g., per_page, cursor)

Returns:
Paginated list of initiatives
"""
response = self._get(f"{workspace_slug}/initiatives", params=params)
return PaginatedInitiativeResponse.model_validate(response)

60 changes: 60 additions & 0 deletions plane/api/initiatives/epics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from collections.abc import Iterable, Mapping
from typing import Any

from ...models.epics import Epic, PaginatedEpicResponse
from ..base_resource import BaseResource


class InitiativeEpics(BaseResource):
"""API client for managing epics associated with initiatives."""

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

def list(
self, workspace_slug: str, initiative_id: str, params: Mapping[str, Any] | None = None
) -> PaginatedEpicResponse:
"""List epics associated with an initiative.

Args:
workspace_slug: The workspace slug identifier
initiative_id: UUID of the initiative
params: Optional query parameters (e.g., per_page, cursor)

Returns:
Paginated list of epics
"""
response = self._get(f"{workspace_slug}/initiatives/{initiative_id}/epics", params=params)
return PaginatedEpicResponse.model_validate(response)

def add(
self, workspace_slug: str, initiative_id: str, epic_ids: Iterable[str]
) -> Iterable[Epic]:
"""Add epics to an initiative.

Args:
workspace_slug: The workspace slug identifier
initiative_id: UUID of the initiative
epic_ids: List of epic UUIDs to add

Returns:
List of added epics
"""
response = self._post(
f"{workspace_slug}/initiatives/{initiative_id}/epics",
{"epic_ids": epic_ids},
)
return [Epic.model_validate(epic) for epic in response]

def remove(self, workspace_slug: str, initiative_id: str, epic_ids: Iterable[str]) -> None:
"""Remove epics from an initiative.

Args:
workspace_slug: The workspace slug identifier
initiative_id: UUID of the initiative
epic_ids: List of epic UUIDs to remove
"""
return self._delete(
f"{workspace_slug}/initiatives/{initiative_id}/epics",
{"epic_ids": epic_ids},
)
139 changes: 139 additions & 0 deletions plane/api/initiatives/labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
from collections.abc import Iterable, Mapping
from typing import Any

from ...models.initiatives import (
CreateInitiativeLabel,
InitiativeLabel,
PaginatedInitiativeLabelResponse,
UpdateInitiativeLabel,
)
from ..base_resource import BaseResource


class InitiativeLabels(BaseResource):
"""API client for managing labels associated with initiatives."""

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

def create(self, workspace_slug: str, data: CreateInitiativeLabel) -> InitiativeLabel:
"""Create a new initiative label in the workspace.

Args:
workspace_slug: The workspace slug identifier
data: Initiative label data

Returns:
The created initiative label
"""
response = self._post(
f"{workspace_slug}/initiatives/labels",
data.model_dump(exclude_none=True),
)
return InitiativeLabel.model_validate(response)

def retrieve(self, workspace_slug: str, label_id: str) -> InitiativeLabel:
"""Retrieve an initiative label by ID.

Args:
workspace_slug: The workspace slug identifier
label_id: UUID of the initiative label

Returns:
The requested initiative label
"""
response = self._get(f"{workspace_slug}/initiatives/labels/{label_id}")
return InitiativeLabel.model_validate(response)

def update(
self, workspace_slug: str, label_id: str, data: UpdateInitiativeLabel
) -> InitiativeLabel:
"""Update an initiative label by ID.

Args:
workspace_slug: The workspace slug identifier
label_id: UUID of the initiative label
data: Updated initiative label data

Returns:
The updated initiative label
"""
response = self._patch(
f"{workspace_slug}/initiatives/labels/{label_id}",
data.model_dump(exclude_none=True),
)
return InitiativeLabel.model_validate(response)

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

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

def list(
self, workspace_slug: str, params: Mapping[str, Any] | None = None
) -> PaginatedInitiativeLabelResponse:
"""List initiative labels in the workspace with optional filtering.

Args:
workspace_slug: The workspace slug identifier
params: Optional query parameters (e.g., per_page, cursor)

Returns:
Paginated list of initiative labels
"""
response = self._get(f"{workspace_slug}/initiatives/labels", params=params)
return PaginatedInitiativeLabelResponse.model_validate(response)

def list_labels(
self, workspace_slug: str, initiative_id: str, params: Mapping[str, Any] | None = None
) -> PaginatedInitiativeLabelResponse:
"""List labels associated with an initiative.

Args:
workspace_slug: The workspace slug identifier
initiative_id: UUID of the initiative
params: Optional query parameters (e.g., per_page, cursor)

Returns:
Paginated list of initiative labels
"""
response = self._get(f"{workspace_slug}/initiatives/{initiative_id}/labels", params=params)
return PaginatedInitiativeLabelResponse.model_validate(response)

def add_labels(
self, workspace_slug: str, initiative_id: str, label_ids: Iterable[str]
) -> Iterable[InitiativeLabel]:
"""Add labels to an initiative.

Args:
workspace_slug: The workspace slug identifier
initiative_id: UUID of the initiative
label_ids: List of label UUIDs to add

Returns:
List of added initiative labels
"""
response = self._post(
f"{workspace_slug}/initiatives/{initiative_id}/labels",
{"label_ids": label_ids},
)
return [InitiativeLabel.model_validate(label) for label in response]

def remove_labels(
self, workspace_slug: str, initiative_id: str, label_ids: Iterable[str]
) -> None:
"""Remove labels from an initiative.

Args:
workspace_slug: The workspace slug identifier
initiative_id: UUID of the initiative
label_ids: List of label UUIDs to remove
"""
return self._delete(
f"{workspace_slug}/initiatives/{initiative_id}/labels",
{"label_ids": label_ids},
)
Loading