Skip to content

Commit 125d8f8

Browse files
authored
feat: Add lite list endpoints, project role distribution, and cycle status filtering to the SDK #56
feat: Add lite list endpoints, project role distribution, and cycle status filtering to the SDK
2 parents a492f30 + b911c54 commit 125d8f8

17 files changed

Lines changed: 512 additions & 11 deletions

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,13 @@ while paginated_members.next_page_results:
293293
params=MemberListQueryParams(per_page=1000, cursor=paginated_members.next_cursor),
294294
)
295295
all_members.extend(paginated_members.results)
296+
297+
# Project-role distribution — member counts per role across all active
298+
# (non-archived) projects in the workspace (built-in + custom roles)
299+
distribution = client.workspaces.get_project_role_distribution(workspace_slug)
300+
print(distribution.total_memberships, distribution.total_distinct_members)
301+
for role in distribution.roles:
302+
print(role.slug, role.membership_count, role.distinct_member_count)
296303
```
297304

298305
#### Roles
@@ -372,6 +379,24 @@ members = client.projects.get_members_lite(
372379
workspace_slug, project_id,
373380
params=MemberListQueryParams(per_page=1000),
374381
)
382+
383+
# Paginated "lite" project list (id, identifier, name, icon/emoji, description,
384+
# cover image, archived_at) — for pickers/reference lookups.
385+
from plane.models.query_params import ProjectLiteListQueryParams
386+
387+
lite = client.projects.list_lite(
388+
workspace_slug,
389+
params=ProjectLiteListQueryParams(per_page=1000, order_by="-created_at"),
390+
)
391+
for p in lite.results:
392+
print(p.identifier, p.name)
393+
394+
# NOTE: archived projects are now EXCLUDED by default. Pass include_archived=True
395+
# to restore the previous behavior of listing archived projects too.
396+
lite = client.projects.list_lite(
397+
workspace_slug,
398+
params=ProjectLiteListQueryParams(include_archived=True),
399+
)
375400
```
376401

377402
#### Work Items
@@ -482,6 +507,26 @@ cycle = client.cycles.create(
482507
# List cycles
483508
cycles = client.cycles.list(workspace_slug, project_id)
484509

510+
# Filter cycles by status: current | upcoming | completed | draft | incomplete.
511+
# `status` is canonical; `cycle_view` is a deprecated alias (status wins if both set).
512+
from plane.models.query_params import CycleListQueryParams
513+
514+
upcoming = client.cycles.list(
515+
workspace_slug, project_id,
516+
params=CycleListQueryParams(status="upcoming"),
517+
)
518+
for c in upcoming.results: # paginated envelope
519+
print(c.name)
520+
521+
# NOTE: status="current" is a special case — the API returns a BARE LIST of cycles
522+
# (not the paginated envelope). list() returns whichever shape the server sends.
523+
current = client.cycles.list(
524+
workspace_slug, project_id,
525+
params=CycleListQueryParams(status="current"),
526+
)
527+
for c in current: # plain list[Cycle]
528+
print(c.name)
529+
485530
# Retrieve a cycle
486531
cycle = client.cycles.retrieve(workspace_slug, project_id, cycle_id)
487532

@@ -499,6 +544,19 @@ client.cycles.delete(workspace_slug, project_id, cycle_id)
499544
# List archived cycles
500545
archived = client.cycles.list_archived(workspace_slug, project_id)
501546

547+
# Paginated "lite" cycle list (full cycle fields minus issue-count metrics).
548+
# Supports a status filter: current | upcoming | completed | draft | incomplete
549+
# (omit for all). The lite endpoint takes only `status` (no `cycle_view` alias)
550+
# and ALWAYS paginates — even for status="current".
551+
from plane.models.query_params import CycleLiteListQueryParams
552+
553+
lite = client.cycles.list_lite(
554+
workspace_slug, project_id,
555+
params=CycleLiteListQueryParams(status="current", per_page=1000),
556+
)
557+
for c in lite.results:
558+
print(c.name)
559+
502560
# Add work items to cycle
503561
from plane.models.cycles import AddWorkItemsToCycleRequest
504562

@@ -557,6 +615,16 @@ client.modules.delete(workspace_slug, project_id, module_id)
557615
# List archived modules
558616
archived = client.modules.list_archived(workspace_slug, project_id)
559617

618+
# Paginated "lite" module list (full module fields minus issue-count metrics)
619+
from plane.models.query_params import LiteListQueryParams
620+
621+
lite = client.modules.list_lite(
622+
workspace_slug, project_id,
623+
params=LiteListQueryParams(per_page=1000, order_by="-created_at"),
624+
)
625+
for m in lite.results:
626+
print(m.name)
627+
560628
# Add work items to module
561629
from plane.models.modules import AddWorkItemsToModuleRequest
562630

plane/api/cycles.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55
CreateCycle,
66
Cycle,
77
PaginatedArchivedCycleResponse,
8+
PaginatedCycleLiteResponse,
89
PaginatedCycleResponse,
910
PaginatedCycleWorkItemResponse,
1011
TransferCycleWorkItemsRequest,
1112
UpdateCycle,
1213
)
13-
from ..models.query_params import WorkItemQueryParams
14+
from ..models.query_params import (
15+
CycleListQueryParams,
16+
CycleLiteListQueryParams,
17+
WorkItemQueryParams,
18+
)
1419
from .base_resource import BaseResource
1520
from .work_items.base import prepare_work_item_params
1621

@@ -72,18 +77,73 @@ def delete(self, workspace_slug: str, project_id: str, cycle_id: str) -> None:
7277
return self._delete(f"{workspace_slug}/projects/{project_id}/cycles/{cycle_id}")
7378

7479
def list(
75-
self, workspace_slug: str, project_id: str, params: Mapping[str, Any] | None = None
76-
) -> PaginatedCycleResponse:
80+
self,
81+
workspace_slug: str,
82+
project_id: str,
83+
params: CycleListQueryParams | Mapping[str, Any] | None = None,
84+
) -> PaginatedCycleResponse | list[Cycle]:
7785
"""List cycles with optional filtering parameters.
7886
87+
Supports cycle status filtering via :class:`CycleListQueryParams`. Pass
88+
``status`` (canonical) or the deprecated ``cycle_view`` alias with one of
89+
``current``, ``upcoming``, ``completed``, ``draft``, ``incomplete``; if
90+
both are supplied the server uses ``status``.
91+
92+
.. note::
93+
With ``status=current`` (or ``cycle_view=current``) the server
94+
returns a **bare list** of :class:`Cycle` objects instead of the
95+
paginated :class:`PaginatedCycleResponse` envelope returned for all
96+
other values. This method returns whichever shape the server sends.
97+
The :meth:`list_lite` endpoint always paginates, even for
98+
``status=current``.
99+
79100
Args:
80101
workspace_slug: The workspace slug identifier
81102
project_id: UUID of the project
82-
params: Optional query parameters
103+
params: Optional query parameters. Prefer ``CycleListQueryParams``;
104+
a plain mapping is also accepted for backwards compatibility.
83105
"""
84-
response = self._get(f"{workspace_slug}/projects/{project_id}/cycles", params=params)
106+
if isinstance(params, CycleListQueryParams):
107+
query_params: Mapping[str, Any] | None = params.to_query_params()
108+
else:
109+
query_params = params
110+
response = self._get(f"{workspace_slug}/projects/{project_id}/cycles", params=query_params)
111+
if isinstance(response, list):
112+
return [Cycle.model_validate(item) for item in response]
85113
return PaginatedCycleResponse.model_validate(response)
86114

115+
def list_lite(
116+
self,
117+
workspace_slug: str,
118+
project_id: str,
119+
params: CycleLiteListQueryParams | None = None,
120+
) -> PaginatedCycleLiteResponse:
121+
"""List cycles as a paginated "lite" response.
122+
123+
Calls the read-only ``/cycles-lite/`` endpoint, which returns the full
124+
cycle field set minus the issue-count metric annotations (total_issues,
125+
completed_issues, etc.), suitable for pickers and reference lookups.
126+
Supports ordering, cursor pagination, and a ``status`` filter -- there
127+
are no field filters. ``per_page`` defaults to and caps at 1000. Unlike
128+
the full cycles list, the lite endpoint accepts only ``status`` (no
129+
``cycle_view`` alias).
130+
131+
Unlike the full ``cycles`` list endpoint (where ``status=current``
132+
returns a bare array), this endpoint always returns the paginated
133+
envelope for every ``status`` value.
134+
135+
Args:
136+
workspace_slug: The workspace slug identifier
137+
project_id: UUID of the project
138+
params: Optional ordering + cursor pagination query parameters,
139+
plus the ``status`` filter
140+
"""
141+
response = self._get(
142+
f"{workspace_slug}/projects/{project_id}/cycles-lite",
143+
params=params.to_query_params() if params else None,
144+
)
145+
return PaginatedCycleLiteResponse.model_validate(response)
146+
87147
def list_archived(
88148
self, workspace_slug: str, project_id: str, params: Mapping[str, Any] | None = None
89149
) -> PaginatedArchivedCycleResponse:

plane/api/modules.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
CreateModule,
66
Module,
77
PaginatedArchivedModuleResponse,
8+
PaginatedModuleLiteResponse,
89
PaginatedModuleResponse,
910
PaginatedModuleWorkItemResponse,
1011
UpdateModule,
1112
)
12-
from ..models.query_params import WorkItemQueryParams
13+
from ..models.query_params import LiteListQueryParams, WorkItemQueryParams
1314
from .base_resource import BaseResource
1415
from .work_items.base import prepare_work_item_params
1516

@@ -83,6 +84,31 @@ def list(
8384
response = self._get(f"{workspace_slug}/projects/{project_id}/modules", params=params)
8485
return PaginatedModuleResponse.model_validate(response)
8586

87+
def list_lite(
88+
self,
89+
workspace_slug: str,
90+
project_id: str,
91+
params: LiteListQueryParams | None = None,
92+
) -> PaginatedModuleLiteResponse:
93+
"""List modules as a paginated "lite" response.
94+
95+
Calls the read-only ``/modules-lite/`` endpoint, which returns the full
96+
module field set minus the issue-count metric annotations (total_issues,
97+
completed_issues, etc.), suitable for pickers and reference lookups.
98+
Only ordering and cursor pagination are supported -- there are no field
99+
filters. ``per_page`` defaults to and caps at 1000.
100+
101+
Args:
102+
workspace_slug: The workspace slug identifier
103+
project_id: UUID of the project
104+
params: Optional ordering + cursor pagination query parameters
105+
"""
106+
response = self._get(
107+
f"{workspace_slug}/projects/{project_id}/modules-lite",
108+
params=params.to_query_params() if params else None,
109+
)
110+
return PaginatedModuleLiteResponse.model_validate(response)
111+
86112
def list_archived(
87113
self, workspace_slug: str, project_id: str, params: Mapping[str, Any] | None = None
88114
) -> PaginatedArchivedModuleResponse:

plane/api/projects.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from ..models.projects import (
77
CreateProject,
8+
PaginatedProjectLiteResponse,
89
PaginatedProjectMemberResponse,
910
PaginatedProjectResponse,
1011
Project,
@@ -17,6 +18,7 @@
1718
MemberListQueryParams,
1819
MemberQueryParams,
1920
PaginatedQueryParams,
21+
ProjectLiteListQueryParams,
2022
)
2123
from .base_resource import BaseResource
2224

@@ -80,6 +82,34 @@ def list(
8082
response = self._get(f"{workspace_slug}/projects", params=query_params)
8183
return PaginatedProjectResponse.model_validate(response)
8284

85+
def list_lite(
86+
self, workspace_slug: str, params: ProjectLiteListQueryParams | None = None
87+
) -> PaginatedProjectLiteResponse:
88+
"""List projects as a paginated "lite" response.
89+
90+
Calls the read-only ``/projects-lite/`` endpoint, which returns a
91+
field-trimmed shape (id, identifier, name, cover_image, icon_prop,
92+
emoji, description, cover_image_url, archived_at) suitable for pickers
93+
and reference lookups. Supports ordering, cursor pagination, and an
94+
``include_archived`` toggle -- there are no field filters. ``per_page``
95+
defaults to and caps at 1000.
96+
97+
.. note::
98+
Archived projects are now **excluded** by default. Pass
99+
``ProjectLiteListQueryParams(include_archived=True)`` to restore the
100+
previous behavior of listing archived projects too.
101+
102+
Args:
103+
workspace_slug: The workspace slug identifier
104+
params: Optional ordering + cursor pagination query parameters,
105+
plus the ``include_archived`` toggle
106+
"""
107+
response = self._get(
108+
f"{workspace_slug}/projects-lite",
109+
params=params.to_query_params() if params else None,
110+
)
111+
return PaginatedProjectLiteResponse.model_validate(response)
112+
83113
def get_worklog_summary(self, workspace_slug: str, project_id: str) -> [ProjectWorklogSummary]:
84114
"""Get work log summary for a project.
85115

plane/api/workspaces.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from ..models.query_params import MemberListQueryParams, MemberQueryParams
66
from ..models.workspaces import (
77
PaginatedWorkspaceMemberResponse,
8+
ProjectRoleDistribution,
89
WorkspaceFeature,
910
WorkspaceMember,
1011
)
@@ -54,6 +55,19 @@ def get_members_lite(
5455
)
5556
return PaginatedWorkspaceMemberResponse.model_validate(response)
5657

58+
def get_project_role_distribution(self, workspace_slug: str) -> ProjectRoleDistribution:
59+
"""Get the distribution of project members by role across the workspace.
60+
61+
Aggregates member counts per role over all active (non-archived)
62+
projects in the workspace. Both built-in roles (admin, contributor,
63+
commenter, guest) and custom roles are included.
64+
65+
Args:
66+
workspace_slug: The workspace slug identifier
67+
"""
68+
response = self._get(f"{workspace_slug}/project-role-distribution")
69+
return ProjectRoleDistribution.model_validate(response)
70+
5771
def get_features(self, workspace_slug: str) -> WorkspaceFeature:
5872
"""Get features of a workspace.
5973

plane/models/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@
1515
)
1616
from .query_params import (
1717
BaseQueryParams,
18+
CycleLiteListQueryParams,
19+
CycleListQueryParams,
20+
LiteListQueryParams,
1821
MemberListQueryParams,
1922
MemberQueryParams,
2023
PaginatedQueryParams,
24+
ProjectLiteListQueryParams,
2125
RetrieveQueryParams,
2226
WorkItemQueryParams,
2327
)
@@ -39,9 +43,13 @@
3943
"IntakeWorkItemStatusEnum",
4044
# query params
4145
"BaseQueryParams",
46+
"CycleLiteListQueryParams",
47+
"CycleListQueryParams",
48+
"LiteListQueryParams",
4249
"MemberListQueryParams",
4350
"MemberQueryParams",
4451
"PaginatedQueryParams",
52+
"ProjectLiteListQueryParams",
4553
"RetrieveQueryParams",
4654
"WorkItemQueryParams",
4755
]

plane/models/cycles.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ class PaginatedCycleResponse(PaginatedResponse):
141141
results: list[Cycle]
142142

143143

144+
class PaginatedCycleLiteResponse(PaginatedResponse):
145+
"""Paginated response for the cycles-lite endpoint."""
146+
147+
model_config = ConfigDict(extra="allow", populate_by_name=True)
148+
149+
results: list[CycleLite]
150+
151+
144152
class PaginatedArchivedCycleResponse(PaginatedResponse):
145153
"""Paginated response for archived cycles."""
146154

plane/models/enums.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@
4848
"FORMULA",
4949
]
5050
RelationTypeEnum = Literal["ISSUE", "USER", "RELEASE"]
51+
CycleStatusEnum = Literal["current", "upcoming", "completed", "draft", "incomplete"]
52+
# Deprecated alias for CycleStatusEnum. ``status`` is the canonical cycle filter
53+
# going forward; ``cycle_view`` is kept only for backward compatibility.
54+
CycleViewEnum = CycleStatusEnum
5155

5256

5357
# Proper Enum classes for better type safety and IDE support
@@ -93,6 +97,7 @@ class InitiativeState(Enum):
9397
COMPLETED = "COMPLETED"
9498
CLOSED = "CLOSED"
9599

100+
96101
class WorkItemRelationType(Enum):
97102
"""Work item relation type enumeration."""
98103

@@ -580,6 +585,8 @@ class Group(Enum):
580585
"PriorityEnum",
581586
"PropertyTypeEnum",
582587
"RelationTypeEnum",
588+
"CycleStatusEnum",
589+
"CycleViewEnum",
583590
"TimezoneEnum",
584591
"TypeMimeEnum",
585592
"NetworkEnum",

0 commit comments

Comments
 (0)