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
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -747,10 +747,56 @@ The SDK provides comprehensive Pydantic v2 models for all API operations.
### Query Parameters

- `BaseQueryParams` - Base query parameters
- `PaginatedQueryParams` - Pagination support (per_page, page)
- `WorkItemQueryParams` - Work item specific queries (expand, order_by, etc.)
- `PaginatedQueryParams` - Cursor-based pagination support (cursor, per_page)
- `WorkItemQueryParams` - Work item specific queries (expand, order_by, `filters`, `pql`, etc.)
- `RetrieveQueryParams` - Retrieve operations (expand, fields, etc.)

#### Filtering work items

`WorkItemQueryParams` accepts two filter inputs that map to the same backend filter engine:

- **`filters`** — a structured filter expression (dict). Supports nested
`and` / `or` / `not` groups and field operators (`__in`, `__gte`,
`__range`, `__icontains`, etc.). The SDK JSON-encodes this into the
`filters=` query parameter.
- **`pql`** — a Plane Query Language string. Human-readable alternative
with the same expressive power.

```python
from plane.models.query_params import WorkItemQueryParams

# Project-scoped, structured filters
client.work_items.list(
"my-workspace",
"project-id",
params=WorkItemQueryParams(
filters={"and": [
{"priority": "urgent"},
{"state_group__in": ["unstarted", "started"]},
]},
order_by="-created_at",
per_page=50,
),
)

# Project-scoped, PQL
client.work_items.list(
"my-workspace",
"project-id",
params=WorkItemQueryParams(pql='priority = "urgent" AND assignee = currentUser()'),
)

# Workspace-scoped — spans every project the caller can view, with
# per-project authorization honored server-side
client.work_items.list_workspace(
"my-workspace",
params=WorkItemQueryParams(filters={"priority": "urgent"}),
)
```

The same `filters` and `pql` query parameters also work on `list_archived`,
`cycles.list_work_items`, and `modules.list_work_items`.

### Response Models

Paginated responses follow the pattern `Paginated<Resource>Response` and include:
Expand Down
23 changes: 14 additions & 9 deletions plane/api/cycles.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
TransferCycleWorkItemsRequest,
UpdateCycle,
)
from ..models.query_params import WorkItemQueryParams
from .base_resource import BaseResource
from .work_items.base import prepare_work_item_params


class Cycles(BaseResource):
Expand Down Expand Up @@ -137,18 +139,25 @@ def list_work_items(
workspace_slug: str,
project_id: str,
cycle_id: str,
params: Mapping[str, Any] | None = None,
params: WorkItemQueryParams | Mapping[str, Any] | None = None,
) -> PaginatedCycleWorkItemResponse:
"""List work items in a cycle.

Supports the same ``filters`` and ``pql`` query parameters as
:meth:`WorkItems.list`. ``filters`` is JSON-encoded into the query
string for both the DTO and the mapping path, so callers can pass
a dict either way.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
cycle_id: UUID of the cycle
params: Optional query parameters
params: Optional query parameters. Prefer ``WorkItemQueryParams``;
a plain mapping is also accepted for backwards compatibility.
"""
response = self._get(
f"{workspace_slug}/projects/{project_id}/cycles/{cycle_id}/cycle-issues", params=params
f"{workspace_slug}/projects/{project_id}/cycles/{cycle_id}/cycle-issues",
params=prepare_work_item_params(params),
)
return PaginatedCycleWorkItemResponse.model_validate(response)

Expand Down Expand Up @@ -180,9 +189,7 @@ def archive(self, workspace_slug: str, project_id: str, cycle_id: str) -> bool:
project_id: UUID of the project
cycle_id: UUID of the cycle
"""
self._post(
f"{workspace_slug}/projects/{project_id}/cycles/{cycle_id}/archive", {}
)
self._post(f"{workspace_slug}/projects/{project_id}/cycles/{cycle_id}/archive", {})
return True

def unarchive(self, workspace_slug: str, project_id: str, cycle_id: str) -> bool:
Expand All @@ -193,7 +200,5 @@ def unarchive(self, workspace_slug: str, project_id: str, cycle_id: str) -> bool
project_id: UUID of the project
cycle_id: UUID of the cycle
"""
self._delete(
f"{workspace_slug}/projects/{project_id}/archived-cycles/{cycle_id}/unarchive"
)
self._delete(f"{workspace_slug}/projects/{project_id}/archived-cycles/{cycle_id}/unarchive")
return True
14 changes: 11 additions & 3 deletions plane/api/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
PaginatedModuleWorkItemResponse,
UpdateModule,
)
from ..models.query_params import WorkItemQueryParams
from .base_resource import BaseResource
from .work_items.base import prepare_work_item_params


class Modules(BaseResource):
Expand Down Expand Up @@ -136,19 +138,25 @@ def list_work_items(
workspace_slug: str,
project_id: str,
module_id: str,
params: Mapping[str, Any] | None = None,
params: WorkItemQueryParams | Mapping[str, Any] | None = None,
) -> PaginatedModuleWorkItemResponse:
"""List work items in a module.

Supports the same ``filters`` and ``pql`` query parameters as
:meth:`WorkItems.list`. ``filters`` is JSON-encoded into the query
string for both the DTO and the mapping path, so callers can pass
a dict either way.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
module_id: UUID of the module
params: Optional query parameters
params: Optional query parameters. Prefer ``WorkItemQueryParams``;
a plain mapping is also accepted for backwards compatibility.
"""
response = self._get(
f"{workspace_slug}/projects/{project_id}/modules/{module_id}/module-issues",
params=params,
params=prepare_work_item_params(params),
)
return PaginatedModuleWorkItemResponse.model_validate(response)

Expand Down
97 changes: 83 additions & 14 deletions plane/api/work_items/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import json
from collections.abc import Mapping
from typing import Any

from ...models.query_params import RetrieveQueryParams, WorkItemQueryParams
Expand All @@ -23,6 +25,28 @@
from .work_logs import WorkLogs


def prepare_work_item_params(
params: WorkItemQueryParams | Mapping[str, Any] | None,
) -> dict[str, Any] | None:
"""Serialize work-item query params for use as HTTP query params.

Accepts either a :class:`WorkItemQueryParams` DTO or a plain mapping,
and normalises the ``filters`` field: the API expects it as a JSON
string in a single ``filters=`` query parameter, but callers are free
to pass it as a dict for ergonomics. Everything else is passed through
as-is by ``requests``' query-string encoder.
"""
if params is None:
return None
if isinstance(params, WorkItemQueryParams):
payload: dict[str, Any] = params.model_dump(exclude_none=True)
else:
payload = {k: v for k, v in params.items() if v is not None}
if "filters" in payload and isinstance(payload["filters"], dict):
payload["filters"] = json.dumps(payload["filters"], separators=(",", ":"))
return payload


class WorkItems(BaseResource):
def __init__(self, config: Any) -> None:
super().__init__(config, "/workspaces/")
Expand Down Expand Up @@ -157,23 +181,67 @@ def list(
project_id: UUID of the project
params: Optional query parameters for filtering, ordering, and pagination

Example:
from plane.models.schemas import WorkItemQueryParams
Example::

from plane.models.query_params import WorkItemQueryParams

# PQL filter (human-readable)
work_items = client.work_items.list(
"my-workspace",
"project-id",
params=WorkItemQueryParams(pql='priority = "urgent"'),
)

# List work items with filters
# Structured `filters` (JSON-encoded into the query string)
work_items = client.work_items.list(
"my-workspace",
"project-id",
params=WorkItemQueryParams(
priority="high",
state="state-id",
expand="assignees,labels"
)
filters={"and": [
{"priority": "urgent"},
{"state_group__in": ["unstarted", "started"]},
]},
),
)
"""
query_params = params.model_dump(exclude_none=True) if params else None
response = self._get(
f"{workspace_slug}/projects/{project_id}/work-items", params=query_params
f"{workspace_slug}/projects/{project_id}/work-items",
params=prepare_work_item_params(params),
)
return PaginatedWorkItemResponse.model_validate(response)

def list_workspace(
self,
workspace_slug: str,
params: WorkItemQueryParams | None = None,
) -> PaginatedWorkItemResponse:
"""List work items across an entire workspace.

Returns a paginated envelope of work items the caller can view,
spanning every project in the workspace (per-project authorization
and conditional grants are honored server-side).

Args:
workspace_slug: The workspace slug identifier
params: Optional query parameters — supports ``filters``, ``pql``,
``order_by``, ``cursor``, ``per_page``, ``fields``, ``expand``.

Example::

from plane.models.query_params import WorkItemQueryParams

results = client.work_items.list_workspace(
"my-workspace",
params=WorkItemQueryParams(
filters={"priority": "urgent"},
order_by="-created_at",
per_page=50,
),
)
"""
response = self._get(
f"{workspace_slug}/work-items",
params=prepare_work_item_params(params),
)
return PaginatedWorkItemResponse.model_validate(response)

Expand Down Expand Up @@ -247,14 +315,17 @@ def list_archived(
) -> PaginatedWorkItemResponse:
"""List archived work items in a project.

Supports the same ``filters`` and ``pql`` query parameters as
:meth:`list`.

Args:
workspace_slug: The workspace slug identifier
project_id: UUID of the project
params: Optional query parameters for filtering, ordering, and pagination
"""
query_params = params.model_dump(exclude_none=True) if params else None
response = self._get(
f"{workspace_slug}/projects/{project_id}/archived-work-items", params=query_params
f"{workspace_slug}/projects/{project_id}/archived-work-items",
params=prepare_work_item_params(params),
)
return PaginatedWorkItemResponse.model_validate(response)

Expand Down Expand Up @@ -286,6 +357,4 @@ def unarchive(self, workspace_slug: str, project_id: str, work_item_id: str) ->
Returns:
None (HTTP 204 No Content)
"""
self._delete(
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/unarchive"
)
self._delete(f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/unarchive")
22 changes: 20 additions & 2 deletions plane/models/query_params.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Query parameter DTOs for list/retrieve endpoints."""

from typing import Any

from pydantic import BaseModel, ConfigDict, Field


Expand Down Expand Up @@ -58,12 +60,28 @@ class WorkItemQueryParams(PaginatedQueryParams):
- fields: Comma-separated fields to include
- order_by: Field to order by (prefix with '-' for descending)
- per_page: Number of results per page (1-100)
- pql: PQL filters
- pql: Plane Query Language expression for structured filtering
- filters: JSON-serializable filter expression for structured filtering
"""

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

pql: str | None = Field(None, description="PQL filters")
pql: str | None = Field(
None,
description=(
"Plane Query Language expression. Human-readable alternative to "
'`filters`. Example: `priority = "urgent" AND assignee = currentUser()`.'
),
)
filters: dict[str, Any] | None = Field(
None,
description=(
"Structured filter expression. Supports nested `and`/`or`/`not` groups "
"and field comparisons with operators like `__in`, `__gte`, `__range`, "
"`__isnull`, `__icontains`, etc. JSON-encoded into the `filters=` "
"query param by the client."
),
)


class RetrieveQueryParams(BaseQueryParams):
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "plane-sdk"
version = "0.2.12"
version = "0.2.13"
description = "Python SDK for Plane API"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
Loading