From c770094cd82a849e024e44995dcdf43a278b9842 Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Fri, 5 Jun 2026 17:08:27 +0530 Subject: [PATCH 1/3] feat: added work items count endpoint --- plane/api/work_items/base.py | 83 ++++++++++++++++++++++++++++++++---- plane/models/query_params.py | 59 ++++++++++++++++++++++++- plane/models/work_items.py | 38 +++++++++++++++++ 3 files changed, 170 insertions(+), 10 deletions(-) diff --git a/plane/api/work_items/base.py b/plane/api/work_items/base.py index 5120f55..67ab391 100644 --- a/plane/api/work_items/base.py +++ b/plane/api/work_items/base.py @@ -4,7 +4,11 @@ from collections.abc import Mapping from typing import Any -from ...models.query_params import RetrieveQueryParams, WorkItemQueryParams +from ...models.query_params import ( + RetrieveQueryParams, + WorkItemCountQueryParams, + WorkItemQueryParams, +) from ...models.work_items import ( AdvancedSearchResult, AdvancedSearchWorkItem, @@ -12,7 +16,10 @@ PaginatedWorkItemResponse, UpdateWorkItem, WorkItem, + WorkItemCountResponse, WorkItemDetail, + WorkItemFlatCountResponse, + WorkItemGroupedCountResponse, WorkItemSearch, ) from ..base_resource import BaseResource @@ -47,6 +54,21 @@ def prepare_work_item_params( return payload +def prepare_work_item_count_params( + params: WorkItemCountQueryParams | None, +) -> dict[str, Any] | None: + """Serialize work-item count query params for use as HTTP query params. + + Same ``filters`` JSON-encoding logic as :func:`prepare_work_item_params`. + """ + if params is None: + return None + payload: dict[str, Any] = params.model_dump(exclude_none=True) + 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/") @@ -245,22 +267,65 @@ def list_workspace( ) return PaginatedWorkItemResponse.model_validate(response) - def list_workspace( + def count_workspace( self, workspace_slug: str, - params: WorkItemQueryParams | None = None, - ) -> PaginatedWorkItemResponse: - """List work items across the entire workspace. + params: WorkItemCountQueryParams | None = None, + ) -> WorkItemCountResponse: + """Return the count of work items across an entire workspace. + + Without ``group_by`` returns a flat :class:`WorkItemFlatCountResponse` + ``{"count": N}``. + + With ``group_by`` returns a :class:`WorkItemGroupedCountResponse` + ``{"grouped_by": ..., "total_count": N, "results": {group_key: {"count": N}}}``. + Keys in ``results`` are raw ORM field values (UUID strings for FK/M2M + dimensions, plain strings for ``priority`` / ``state__group``, ISO-date + strings for ``target_date`` / ``start_date``). The special key + ``"None"`` represents work items with no value in that dimension. Args: workspace_slug: The workspace slug identifier - params: Optional query parameters for filtering, ordering, and pagination + params: Optional query parameters — supports ``filters``, ``pql``, + and ``group_by``. + + Example:: + + from plane.models.query_params import WorkItemCountQueryParams + + # Flat count + result = client.work_items.count_workspace( + "my-workspace", + params=WorkItemCountQueryParams( + filters={"priority__in": ["urgent", "high"]}, + ), + ) + print(result.count) # e.g. 12 + + # Grouped by priority + result = client.work_items.count_workspace( + "my-workspace", + params=WorkItemCountQueryParams(group_by="priority"), + ) + for group, entry in result.results.items(): + print(f"{group}: {entry.count}") + + # Grouped by state, filtered by PQL + result = client.work_items.count_workspace( + "my-workspace", + params=WorkItemCountQueryParams( + pql='assignee = currentUser()', + group_by="state_id", + ), + ) """ - query_params = params.model_dump(exclude_none=True) if params else None response = self._get( - f"{workspace_slug}/work-items", params=query_params + f"{workspace_slug}/work-items/count", + params=prepare_work_item_count_params(params), ) - return PaginatedWorkItemResponse.model_validate(response) + if "grouped_by" in response: + return WorkItemGroupedCountResponse.model_validate(response) + return WorkItemFlatCountResponse.model_validate(response) def search( self, diff --git a/plane/models/query_params.py b/plane/models/query_params.py index 49fa24b..9705863 100644 --- a/plane/models/query_params.py +++ b/plane/models/query_params.py @@ -1,6 +1,6 @@ """Query parameter DTOs for list/retrieve endpoints.""" -from typing import Any +from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field @@ -90,9 +90,66 @@ class RetrieveQueryParams(BaseQueryParams): model_config = ConfigDict(extra="ignore", populate_by_name=True) +WorkItemCountGroupBy = Literal[ + "state_id", + "state__group", + "priority", + "project_id", + "type_id", + "labels__id", + "assignees__id", + "issue_module__module_id", + "release_work_items__release_id", + "cycle_id", + "milestone_id", + "created_by", + "target_date", + "start_date", +] + + +class WorkItemCountQueryParams(BaseModel): + """Query parameters for the workspace work item count endpoint. + + Accepts the same ``filters`` and ``pql`` as :class:`WorkItemQueryParams` + plus an optional ``group_by`` field. + + Without ``group_by`` the response is ``{"count": N}``. + With ``group_by`` the response is + ``{"grouped_by": ..., "total_count": N, "results": {group_key: {"count": N}}}``. + """ + + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + 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. JSON-encoded into the `filters=` " + "query param by the client." + ), + ) + group_by: WorkItemCountGroupBy | None = Field( + None, + description=( + "ORM field to group counts by. When supplied the response shape " + "changes from a flat ``{count}`` to a grouped " + "``{grouped_by, total_count, results}`` envelope." + ), + ) + + __all__ = [ "BaseQueryParams", "PaginatedQueryParams", "RetrieveQueryParams", + "WorkItemCountGroupBy", + "WorkItemCountQueryParams", "WorkItemQueryParams", ] diff --git a/plane/models/work_items.py b/plane/models/work_items.py index 09c91a0..15fda04 100644 --- a/plane/models/work_items.py +++ b/plane/models/work_items.py @@ -594,3 +594,41 @@ class PaginatedWorkItemLinkResponse(PaginatedResponse): model_config = ConfigDict(extra="allow", populate_by_name=True) results: list[WorkItemLink] + + +class WorkItemGroupCountEntry(BaseModel): + """Count for a single group in a grouped count response.""" + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + count: int + + +class WorkItemFlatCountResponse(BaseModel): + """Response from the workspace work item count endpoint when ``group_by`` + is not supplied.""" + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + count: int + + +class WorkItemGroupedCountResponse(BaseModel): + """Response from the workspace work item count endpoint when ``group_by`` + is supplied. + + ``results`` keys are raw ORM field values: UUID strings for FK/M2M + dimensions, plain strings for ``priority`` / ``state__group``, and + ISO-date strings for ``target_date`` / ``start_date``. The special + key ``"None"`` is used for work items with no value in that dimension + (unassigned, unlabelled, no release, etc.). + """ + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + grouped_by: str + total_count: int + results: dict[str, WorkItemGroupCountEntry] + + +WorkItemCountResponse = WorkItemFlatCountResponse | WorkItemGroupedCountResponse From 0f432b0e9ab928b578c6a4221eb55fe639239c0b Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Fri, 5 Jun 2026 18:26:29 +0530 Subject: [PATCH 2/3] feat: update work item count response to always return grouped counts --- plane/api/work_items/base.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/plane/api/work_items/base.py b/plane/api/work_items/base.py index 67ab391..dfb5bd6 100644 --- a/plane/api/work_items/base.py +++ b/plane/api/work_items/base.py @@ -18,7 +18,6 @@ WorkItem, WorkItemCountResponse, WorkItemDetail, - WorkItemFlatCountResponse, WorkItemGroupedCountResponse, WorkItemSearch, ) @@ -274,15 +273,13 @@ def count_workspace( ) -> WorkItemCountResponse: """Return the count of work items across an entire workspace. - Without ``group_by`` returns a flat :class:`WorkItemFlatCountResponse` - ``{"count": N}``. + Always returns :class:`WorkItemGroupedCountResponse` with fields + ``grouped_by``, ``total_count``, and ``grouped_counts``. - With ``group_by`` returns a :class:`WorkItemGroupedCountResponse` - ``{"grouped_by": ..., "total_count": N, "results": {group_key: {"count": N}}}``. - Keys in ``results`` are raw ORM field values (UUID strings for FK/M2M - dimensions, plain strings for ``priority`` / ``state__group``, ISO-date - strings for ``target_date`` / ``start_date``). The special key - ``"None"`` represents work items with no value in that dimension. + ``grouped_counts`` keys are raw ORM field values: UUID strings for + FK/M2M dimensions, plain strings for ``priority`` / ``state__group``, + ISO-date strings for ``target_date`` / ``start_date``. ``"None"`` is + used for work items with no value in that dimension. Args: workspace_slug: The workspace slug identifier @@ -293,21 +290,21 @@ def count_workspace( from plane.models.query_params import WorkItemCountQueryParams - # Flat count + # Total count result = client.work_items.count_workspace( "my-workspace", params=WorkItemCountQueryParams( filters={"priority__in": ["urgent", "high"]}, ), ) - print(result.count) # e.g. 12 + print(result.total_count) # e.g. 12 # Grouped by priority result = client.work_items.count_workspace( "my-workspace", params=WorkItemCountQueryParams(group_by="priority"), ) - for group, entry in result.results.items(): + for group, entry in result.grouped_counts.items(): print(f"{group}: {entry.count}") # Grouped by state, filtered by PQL @@ -323,9 +320,7 @@ def count_workspace( f"{workspace_slug}/work-items/count", params=prepare_work_item_count_params(params), ) - if "grouped_by" in response: - return WorkItemGroupedCountResponse.model_validate(response) - return WorkItemFlatCountResponse.model_validate(response) + return WorkItemGroupedCountResponse.model_validate(response) def search( self, From eac359fadca4fe01282baed91ede79ba53abbe8a Mon Sep 17 00:00:00 2001 From: sangeethailango Date: Fri, 5 Jun 2026 18:27:34 +0530 Subject: [PATCH 3/3] fix: update docs --- plane/models/work_items.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plane/models/work_items.py b/plane/models/work_items.py index 15fda04..44a7329 100644 --- a/plane/models/work_items.py +++ b/plane/models/work_items.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Any -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, model_validator from .enums import AccessEnum, PriorityEnum, WorkItemRelationTypeEnum from .labels import Label @@ -604,31 +604,31 @@ class WorkItemGroupCountEntry(BaseModel): count: int -class WorkItemFlatCountResponse(BaseModel): - """Response from the workspace work item count endpoint when ``group_by`` - is not supplied.""" +class WorkItemGroupedCountResponse(BaseModel): + """Response from the workspace work item count endpoint. - model_config = ConfigDict(extra="allow", populate_by_name=True) + Handles both response shapes from ``GET /workspaces//work-items/count``: - count: int + **Grouped** (with ``group_by`` param):: - -class WorkItemGroupedCountResponse(BaseModel): - """Response from the workspace work item count endpoint when ``group_by`` - is supplied. + { + "grouped_by": "priority", + "total_count": 42, + "results": {"urgent": {"count": 3}, "none": {"count": 6}} + } ``results`` keys are raw ORM field values: UUID strings for FK/M2M dimensions, plain strings for ``priority`` / ``state__group``, and ISO-date strings for ``target_date`` / ``start_date``. The special - key ``"None"`` is used for work items with no value in that dimension - (unassigned, unlabelled, no release, etc.). + key ``"None"`` represents work items with no value in that dimension. """ model_config = ConfigDict(extra="allow", populate_by_name=True) - grouped_by: str - total_count: int - results: dict[str, WorkItemGroupCountEntry] + # grouped response + grouped_by: str | None = None + total_count: int | None = None + grouped_counts: dict[str, WorkItemGroupCountEntry] | None = None -WorkItemCountResponse = WorkItemFlatCountResponse | WorkItemGroupedCountResponse +WorkItemCountResponse = WorkItemGroupedCountResponse