Skip to content

Commit d55f45e

Browse files
dheeru0198claude
andcommitted
feat: rich filters + pql on work-item list endpoints, add list_workspace
Expose the new external API's structured filtering capability on every work-item list method in the SDK. - WorkItemQueryParams now accepts `filters: dict[str, Any] | None` (the existing `pql: str | None` field is unchanged). A small `_prepare_work_item_params` helper JSON-encodes `filters` into the `filters=` query string the API expects. - New `WorkItems.list_workspace(workspace_slug, params)` calling `GET /workspaces/<slug>/work-items/` — paginated, total_results, spans every project the caller can view. - `WorkItems.list` and `WorkItems.list_archived` now route through `_prepare_work_item_params`, so `filters` and `pql` work uniformly. - `Cycles.list_work_items` and `Modules.list_work_items` accept `WorkItemQueryParams` (typed path) with the same serialization, and still accept a plain `Mapping[str, Any]` for backwards compatibility. - Real-API unit tests for `filters` on `list`, plus `list_workspace` with and without `filters`. - Bump version to 0.2.13. Backend: makeplane/plane-ee#7376 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 88ade49 commit d55f45e

7 files changed

Lines changed: 245 additions & 30 deletions

File tree

README.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,9 +748,55 @@ The SDK provides comprehensive Pydantic v2 models for all API operations.
748748

749749
- `BaseQueryParams` - Base query parameters
750750
- `PaginatedQueryParams` - Pagination support (per_page, page)
751-
- `WorkItemQueryParams` - Work item specific queries (expand, order_by, etc.)
751+
- `WorkItemQueryParams` - Work item specific queries (expand, order_by, `filters`, `pql`, etc.)
752752
- `RetrieveQueryParams` - Retrieve operations (expand, fields, etc.)
753753

754+
#### Filtering work items
755+
756+
`WorkItemQueryParams` accepts two filter inputs that map to the same backend filter engine:
757+
758+
- **`filters`** — a structured filter expression (dict). Supports nested
759+
`and` / `or` / `not` groups and field operators (`__in`, `__gte`,
760+
`__range`, `__icontains`, etc.). The SDK JSON-encodes this into the
761+
`filters=` query parameter.
762+
- **`pql`** — a Plane Query Language string. Human-readable alternative
763+
with the same expressive power.
764+
765+
```python
766+
from plane.models.query_params import WorkItemQueryParams
767+
768+
# Project-scoped, structured filters
769+
client.work_items.list(
770+
"my-workspace",
771+
"project-id",
772+
params=WorkItemQueryParams(
773+
filters={"and": [
774+
{"priority": "urgent"},
775+
{"state_group__in": ["unstarted", "started"]},
776+
]},
777+
order_by="-created_at",
778+
per_page=50,
779+
),
780+
)
781+
782+
# Project-scoped, PQL
783+
client.work_items.list(
784+
"my-workspace",
785+
"project-id",
786+
params=WorkItemQueryParams(pql='priority = "urgent" AND assignee = currentUser()'),
787+
)
788+
789+
# Workspace-scoped — spans every project the caller can view, with
790+
# per-project authorization honored server-side
791+
client.work_items.list_workspace(
792+
"my-workspace",
793+
params=WorkItemQueryParams(filters={"priority": "urgent"}),
794+
)
795+
```
796+
797+
The same `filters` and `pql` query parameters also work on `list_archived`,
798+
`cycles.list_work_items`, and `modules.list_work_items`.
799+
754800
### Response Models
755801

756802
Paginated responses follow the pattern `Paginated<Resource>Response` and include:

plane/api/cycles.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
TransferCycleWorkItemsRequest,
1111
UpdateCycle,
1212
)
13+
from ..models.query_params import WorkItemQueryParams
1314
from .base_resource import BaseResource
15+
from .work_items.base import _prepare_work_item_params
1416

1517

1618
class Cycles(BaseResource):
@@ -137,18 +139,28 @@ def list_work_items(
137139
workspace_slug: str,
138140
project_id: str,
139141
cycle_id: str,
140-
params: Mapping[str, Any] | None = None,
142+
params: WorkItemQueryParams | Mapping[str, Any] | None = None,
141143
) -> PaginatedCycleWorkItemResponse:
142144
"""List work items in a cycle.
143145
146+
Supports the same ``filters`` and ``pql`` query parameters as
147+
:meth:`WorkItems.list`.
148+
144149
Args:
145150
workspace_slug: The workspace slug identifier
146151
project_id: UUID of the project
147152
cycle_id: UUID of the cycle
148-
params: Optional query parameters
153+
params: Optional query parameters. Prefer ``WorkItemQueryParams``;
154+
a plain mapping is also accepted for backwards compatibility
155+
and is passed through unchanged.
149156
"""
157+
if isinstance(params, WorkItemQueryParams):
158+
query_params: Mapping[str, Any] | None = _prepare_work_item_params(params)
159+
else:
160+
query_params = params
150161
response = self._get(
151-
f"{workspace_slug}/projects/{project_id}/cycles/{cycle_id}/cycle-issues", params=params
162+
f"{workspace_slug}/projects/{project_id}/cycles/{cycle_id}/cycle-issues",
163+
params=query_params,
152164
)
153165
return PaginatedCycleWorkItemResponse.model_validate(response)
154166

@@ -180,9 +192,7 @@ def archive(self, workspace_slug: str, project_id: str, cycle_id: str) -> bool:
180192
project_id: UUID of the project
181193
cycle_id: UUID of the cycle
182194
"""
183-
self._post(
184-
f"{workspace_slug}/projects/{project_id}/cycles/{cycle_id}/archive", {}
185-
)
195+
self._post(f"{workspace_slug}/projects/{project_id}/cycles/{cycle_id}/archive", {})
186196
return True
187197

188198
def unarchive(self, workspace_slug: str, project_id: str, cycle_id: str) -> bool:
@@ -193,7 +203,5 @@ def unarchive(self, workspace_slug: str, project_id: str, cycle_id: str) -> bool
193203
project_id: UUID of the project
194204
cycle_id: UUID of the cycle
195205
"""
196-
self._delete(
197-
f"{workspace_slug}/projects/{project_id}/archived-cycles/{cycle_id}/unarchive"
198-
)
206+
self._delete(f"{workspace_slug}/projects/{project_id}/archived-cycles/{cycle_id}/unarchive")
199207
return True

plane/api/modules.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
PaginatedModuleWorkItemResponse,
1010
UpdateModule,
1111
)
12+
from ..models.query_params import WorkItemQueryParams
1213
from .base_resource import BaseResource
14+
from .work_items.base import _prepare_work_item_params
1315

1416

1517
class Modules(BaseResource):
@@ -136,19 +138,28 @@ def list_work_items(
136138
workspace_slug: str,
137139
project_id: str,
138140
module_id: str,
139-
params: Mapping[str, Any] | None = None,
141+
params: WorkItemQueryParams | Mapping[str, Any] | None = None,
140142
) -> PaginatedModuleWorkItemResponse:
141143
"""List work items in a module.
142144
145+
Supports the same ``filters`` and ``pql`` query parameters as
146+
:meth:`WorkItems.list`.
147+
143148
Args:
144149
workspace_slug: The workspace slug identifier
145150
project_id: UUID of the project
146151
module_id: UUID of the module
147-
params: Optional query parameters
152+
params: Optional query parameters. Prefer ``WorkItemQueryParams``;
153+
a plain mapping is also accepted for backwards compatibility
154+
and is passed through unchanged.
148155
"""
156+
if isinstance(params, WorkItemQueryParams):
157+
query_params: Mapping[str, Any] | None = _prepare_work_item_params(params)
158+
else:
159+
query_params = params
149160
response = self._get(
150161
f"{workspace_slug}/projects/{project_id}/modules/{module_id}/module-issues",
151-
params=params,
162+
params=query_params,
152163
)
153164
return PaginatedModuleWorkItemResponse.model_validate(response)
154165

plane/api/work_items/base.py

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import json
34
from typing import Any
45

56
from ...models.query_params import RetrieveQueryParams, WorkItemQueryParams
@@ -23,6 +24,21 @@
2324
from .work_logs import WorkLogs
2425

2526

27+
def _prepare_work_item_params(params: WorkItemQueryParams | None) -> dict[str, Any] | None:
28+
"""Serialize WorkItemQueryParams for use as HTTP query params.
29+
30+
The ``filters`` field is a structured object but the API expects it as a
31+
JSON string in a single ``filters=`` query parameter. Everything else is
32+
passed through as-is by ``requests``' query-string encoder.
33+
"""
34+
if params is None:
35+
return None
36+
payload = params.model_dump(exclude_none=True)
37+
if "filters" in payload and isinstance(payload["filters"], dict):
38+
payload["filters"] = json.dumps(payload["filters"], separators=(",", ":"))
39+
return payload
40+
41+
2642
class WorkItems(BaseResource):
2743
def __init__(self, config: Any) -> None:
2844
super().__init__(config, "/workspaces/")
@@ -157,23 +173,67 @@ def list(
157173
project_id: UUID of the project
158174
params: Optional query parameters for filtering, ordering, and pagination
159175
160-
Example:
161-
from plane.models.schemas import WorkItemQueryParams
176+
Example::
162177
163-
# List work items with filters
178+
from plane.models.query_params import WorkItemQueryParams
179+
180+
# PQL filter (human-readable)
181+
work_items = client.work_items.list(
182+
"my-workspace",
183+
"project-id",
184+
params=WorkItemQueryParams(pql='priority = "urgent"'),
185+
)
186+
187+
# Structured `filters` (JSON-encoded into the query string)
164188
work_items = client.work_items.list(
165189
"my-workspace",
166190
"project-id",
167191
params=WorkItemQueryParams(
168-
priority="high",
169-
state="state-id",
170-
expand="assignees,labels"
171-
)
192+
filters={"and": [
193+
{"priority": "urgent"},
194+
{"state_group__in": ["unstarted", "started"]},
195+
]},
196+
),
197+
)
198+
"""
199+
response = self._get(
200+
f"{workspace_slug}/projects/{project_id}/work-items",
201+
params=_prepare_work_item_params(params),
202+
)
203+
return PaginatedWorkItemResponse.model_validate(response)
204+
205+
def list_workspace(
206+
self,
207+
workspace_slug: str,
208+
params: WorkItemQueryParams | None = None,
209+
) -> PaginatedWorkItemResponse:
210+
"""List work items across an entire workspace.
211+
212+
Returns a paginated envelope of work items the caller can view,
213+
spanning every project in the workspace (per-project authorization
214+
and conditional grants are honored server-side).
215+
216+
Args:
217+
workspace_slug: The workspace slug identifier
218+
params: Optional query parameters — supports ``filters``, ``pql``,
219+
``order_by``, ``cursor``, ``per_page``, ``fields``, ``expand``.
220+
221+
Example::
222+
223+
from plane.models.query_params import WorkItemQueryParams
224+
225+
results = client.work_items.list_workspace(
226+
"my-workspace",
227+
params=WorkItemQueryParams(
228+
filters={"priority": "urgent"},
229+
order_by="-created_at",
230+
per_page=50,
231+
),
172232
)
173233
"""
174-
query_params = params.model_dump(exclude_none=True) if params else None
175234
response = self._get(
176-
f"{workspace_slug}/projects/{project_id}/work-items", params=query_params
235+
f"{workspace_slug}/work-items",
236+
params=_prepare_work_item_params(params),
177237
)
178238
return PaginatedWorkItemResponse.model_validate(response)
179239

@@ -247,14 +307,17 @@ def list_archived(
247307
) -> PaginatedWorkItemResponse:
248308
"""List archived work items in a project.
249309
310+
Supports the same ``filters`` and ``pql`` query parameters as
311+
:meth:`list`.
312+
250313
Args:
251314
workspace_slug: The workspace slug identifier
252315
project_id: UUID of the project
253316
params: Optional query parameters for filtering, ordering, and pagination
254317
"""
255-
query_params = params.model_dump(exclude_none=True) if params else None
256318
response = self._get(
257-
f"{workspace_slug}/projects/{project_id}/archived-work-items", params=query_params
319+
f"{workspace_slug}/projects/{project_id}/archived-work-items",
320+
params=_prepare_work_item_params(params),
258321
)
259322
return PaginatedWorkItemResponse.model_validate(response)
260323

@@ -286,6 +349,4 @@ def unarchive(self, workspace_slug: str, project_id: str, work_item_id: str) ->
286349
Returns:
287350
None (HTTP 204 No Content)
288351
"""
289-
self._delete(
290-
f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/unarchive"
291-
)
352+
self._delete(f"{workspace_slug}/projects/{project_id}/work-items/{work_item_id}/unarchive")

plane/models/query_params.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Query parameter DTOs for list/retrieve endpoints."""
22

3+
from typing import Any
4+
35
from pydantic import BaseModel, ConfigDict, Field
46

57

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

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

66-
pql: str | None = Field(None, description="PQL filters")
69+
pql: str | None = Field(
70+
None,
71+
description=(
72+
"Plane Query Language expression. Human-readable alternative to "
73+
'`filters`. Example: `priority = "urgent" AND assignee = currentUser()`.'
74+
),
75+
)
76+
filters: dict[str, Any] | None = Field(
77+
None,
78+
description=(
79+
"Structured filter expression. Supports nested `and`/`or`/`not` groups "
80+
"and field comparisons with operators like `__in`, `__gte`, `__range`, "
81+
"`__isnull`, `__icontains`, etc. JSON-encoded into the `filters=` "
82+
"query param by the client."
83+
),
84+
)
6785

6886

6987
class RetrieveQueryParams(BaseQueryParams):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "plane-sdk"
7-
version = "0.2.12"
7+
version = "0.2.13"
88
description = "Python SDK for Plane API"
99
readme = "README.md"
1010
requires-python = ">=3.10"

0 commit comments

Comments
 (0)