From c8a74495d71bfdb1f54f93938e1adf063ec23231 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 25 Jun 2026 15:21:17 +0530 Subject: [PATCH 1/3] chore: implemented lite endpoints for project, cycle, and module --- plane/api/cycles.py | 28 +++++++++++++++++++++++++++- plane/api/modules.py | 28 +++++++++++++++++++++++++++- plane/api/projects.py | 21 +++++++++++++++++++++ plane/models/__init__.py | 2 ++ plane/models/cycles.py | 8 ++++++++ plane/models/modules.py | 8 ++++++++ plane/models/projects.py | 27 +++++++++++++++++++++++++++ plane/models/query_params.py | 28 ++++++++++++++++++++++++++++ tests/unit/test_cycles.py | 16 +++++++++++++++- tests/unit/test_modules.py | 16 +++++++++++++++- tests/unit/test_projects.py | 27 ++++++++++++++++++++++++++- 11 files changed, 204 insertions(+), 5 deletions(-) diff --git a/plane/api/cycles.py b/plane/api/cycles.py index 4ffb998..4a6d857 100644 --- a/plane/api/cycles.py +++ b/plane/api/cycles.py @@ -5,12 +5,13 @@ CreateCycle, Cycle, PaginatedArchivedCycleResponse, + PaginatedCycleLiteResponse, PaginatedCycleResponse, PaginatedCycleWorkItemResponse, TransferCycleWorkItemsRequest, UpdateCycle, ) -from ..models.query_params import WorkItemQueryParams +from ..models.query_params import LiteListQueryParams, WorkItemQueryParams from .base_resource import BaseResource from .work_items.base import prepare_work_item_params @@ -84,6 +85,31 @@ def list( response = self._get(f"{workspace_slug}/projects/{project_id}/cycles", params=params) return PaginatedCycleResponse.model_validate(response) + def list_lite( + self, + workspace_slug: str, + project_id: str, + params: LiteListQueryParams | None = None, + ) -> PaginatedCycleLiteResponse: + """List cycles as a paginated "lite" response. + + Calls the read-only ``/cycles-lite/`` endpoint, which returns the full + cycle field set minus the issue-count metric annotations (total_issues, + completed_issues, etc.), suitable for pickers and reference lookups. + Only ordering and cursor pagination are supported -- there are no field + filters. ``per_page`` defaults to and caps at 1000. + + Args: + workspace_slug: The workspace slug identifier + project_id: UUID of the project + params: Optional ordering + cursor pagination query parameters + """ + query_params = params.model_dump(exclude_none=True) if params else None + response = self._get( + f"{workspace_slug}/projects/{project_id}/cycles-lite", params=query_params + ) + return PaginatedCycleLiteResponse.model_validate(response) + def list_archived( self, workspace_slug: str, project_id: str, params: Mapping[str, Any] | None = None ) -> PaginatedArchivedCycleResponse: diff --git a/plane/api/modules.py b/plane/api/modules.py index 19751a8..463f6e0 100644 --- a/plane/api/modules.py +++ b/plane/api/modules.py @@ -5,11 +5,12 @@ CreateModule, Module, PaginatedArchivedModuleResponse, + PaginatedModuleLiteResponse, PaginatedModuleResponse, PaginatedModuleWorkItemResponse, UpdateModule, ) -from ..models.query_params import WorkItemQueryParams +from ..models.query_params import LiteListQueryParams, WorkItemQueryParams from .base_resource import BaseResource from .work_items.base import prepare_work_item_params @@ -83,6 +84,31 @@ def list( response = self._get(f"{workspace_slug}/projects/{project_id}/modules", params=params) return PaginatedModuleResponse.model_validate(response) + def list_lite( + self, + workspace_slug: str, + project_id: str, + params: LiteListQueryParams | None = None, + ) -> PaginatedModuleLiteResponse: + """List modules as a paginated "lite" response. + + Calls the read-only ``/modules-lite/`` endpoint, which returns the full + module field set minus the issue-count metric annotations (total_issues, + completed_issues, etc.), suitable for pickers and reference lookups. + Only ordering and cursor pagination are supported -- there are no field + filters. ``per_page`` defaults to and caps at 1000. + + Args: + workspace_slug: The workspace slug identifier + project_id: UUID of the project + params: Optional ordering + cursor pagination query parameters + """ + query_params = params.model_dump(exclude_none=True) if params else None + response = self._get( + f"{workspace_slug}/projects/{project_id}/modules-lite", params=query_params + ) + return PaginatedModuleLiteResponse.model_validate(response) + def list_archived( self, workspace_slug: str, project_id: str, params: Mapping[str, Any] | None = None ) -> PaginatedArchivedModuleResponse: diff --git a/plane/api/projects.py b/plane/api/projects.py index 252fb64..9dff63f 100644 --- a/plane/api/projects.py +++ b/plane/api/projects.py @@ -5,6 +5,7 @@ from ..models.projects import ( CreateProject, + PaginatedProjectLiteResponse, PaginatedProjectMemberResponse, PaginatedProjectResponse, Project, @@ -14,6 +15,7 @@ UpdateProject, ) from ..models.query_params import ( + LiteListQueryParams, MemberListQueryParams, MemberQueryParams, PaginatedQueryParams, @@ -80,6 +82,25 @@ def list( response = self._get(f"{workspace_slug}/projects", params=query_params) return PaginatedProjectResponse.model_validate(response) + def list_lite( + self, workspace_slug: str, params: LiteListQueryParams | None = None + ) -> PaginatedProjectLiteResponse: + """List projects as a paginated "lite" response. + + Calls the read-only ``/projects-lite/`` endpoint, which returns a + field-trimmed shape (id, identifier, name, cover_image, icon_prop, + emoji, description, cover_image_url) suitable for pickers and reference + lookups. Only ordering and cursor pagination are supported -- there are + no field filters. ``per_page`` defaults to and caps at 1000. + + Args: + workspace_slug: The workspace slug identifier + params: Optional ordering + cursor pagination query parameters + """ + query_params = params.model_dump(exclude_none=True) if params else None + response = self._get(f"{workspace_slug}/projects-lite", params=query_params) + return PaginatedProjectLiteResponse.model_validate(response) + def get_worklog_summary(self, workspace_slug: str, project_id: str) -> [ProjectWorklogSummary]: """Get work log summary for a project. diff --git a/plane/models/__init__.py b/plane/models/__init__.py index 5121534..c53e77a 100644 --- a/plane/models/__init__.py +++ b/plane/models/__init__.py @@ -15,6 +15,7 @@ ) from .query_params import ( BaseQueryParams, + LiteListQueryParams, MemberListQueryParams, MemberQueryParams, PaginatedQueryParams, @@ -39,6 +40,7 @@ "IntakeWorkItemStatusEnum", # query params "BaseQueryParams", + "LiteListQueryParams", "MemberListQueryParams", "MemberQueryParams", "PaginatedQueryParams", diff --git a/plane/models/cycles.py b/plane/models/cycles.py index 70ad4ac..56d71b5 100644 --- a/plane/models/cycles.py +++ b/plane/models/cycles.py @@ -141,6 +141,14 @@ class PaginatedCycleResponse(PaginatedResponse): results: list[Cycle] +class PaginatedCycleLiteResponse(PaginatedResponse): + """Paginated response for the cycles-lite endpoint.""" + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + results: list[CycleLite] + + class PaginatedArchivedCycleResponse(PaginatedResponse): """Paginated response for archived cycles.""" diff --git a/plane/models/modules.py b/plane/models/modules.py index 6b4d149..d3f3481 100644 --- a/plane/models/modules.py +++ b/plane/models/modules.py @@ -132,6 +132,14 @@ class PaginatedModuleResponse(PaginatedResponse): results: list[Module] +class PaginatedModuleLiteResponse(PaginatedResponse): + """Paginated response for the modules-lite endpoint.""" + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + results: list[ModuleLite] + + class PaginatedArchivedModuleResponse(PaginatedResponse): """Paginated response for archived modules.""" diff --git a/plane/models/projects.py b/plane/models/projects.py index 48ab1c1..e399fe6 100644 --- a/plane/models/projects.py +++ b/plane/models/projects.py @@ -58,6 +58,25 @@ class Project(BaseModel): default_state: str | None = None +class ProjectLite(BaseModel): + """Lite project information. + + Trimmed shape returned by the read-only ``projects-lite`` list endpoint + (``ProjectLiteSerializer``), intended for pickers and reference lookups. + """ + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + id: str | None = None + identifier: str + name: str + cover_image: str | None = None + icon_prop: Any | None = None + emoji: str | None = None + description: str | None = None + cover_image_url: str | None = None + + class CreateProject(BaseModel): """Request model for creating a project.""" @@ -138,6 +157,14 @@ class PaginatedProjectResponse(PaginatedResponse): results: list[Project] +class PaginatedProjectLiteResponse(PaginatedResponse): + """Paginated response for the projects-lite endpoint.""" + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + results: list[ProjectLite] + + class ProjectMember(UserLite): """Project member model. diff --git a/plane/models/query_params.py b/plane/models/query_params.py index 6ce0c24..4e5b02a 100644 --- a/plane/models/query_params.py +++ b/plane/models/query_params.py @@ -154,6 +154,33 @@ class MemberListQueryParams(MemberQueryParams): ) +class LiteListQueryParams(BaseModel): + """Query parameters for the read-only "lite" list endpoints. + + The lite list routes (``projects-lite``, ``cycles-lite``, ``modules-lite``) + support only ordering and cursor pagination -- they expose no field filters. + ``per_page`` defaults to and caps at 1000 on the server. + """ + + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + cursor: str | None = Field( + None, + description='Pagination cursor of the form "{per_page}:{page}:{offset}", ' + "e.g. 1000:0:0. Use the response's next_cursor to fetch the next page.", + ) + per_page: int | None = Field( + None, + description="Number of results per page (default and max 1000)", + ge=1, + le=1000, + ) + order_by: str | None = Field( + None, + description="Field to order results by. Prefix with '-' for descending order", + ) + + WorkItemCountGroupBy = Literal[ "state_id", "state__group", @@ -220,6 +247,7 @@ class WorkItemCountQueryParams(BaseModel): __all__ = [ "BaseQueryParams", + "LiteListQueryParams", "MemberListQueryParams", "MemberQueryParams", "PaginatedQueryParams", diff --git a/tests/unit/test_cycles.py b/tests/unit/test_cycles.py index 7f4b6bf..a02ee0e 100644 --- a/tests/unit/test_cycles.py +++ b/tests/unit/test_cycles.py @@ -5,8 +5,9 @@ import pytest from plane.client import PlaneClient -from plane.models.cycles import CreateCycle, UpdateCycle +from plane.models.cycles import CreateCycle, CycleLite, UpdateCycle from plane.models.projects import Project, ProjectFeature +from plane.models.query_params import LiteListQueryParams class TestCyclesAPI: @@ -22,6 +23,19 @@ def test_list_cycles( assert hasattr(response, "count") assert isinstance(response.results, list) + def test_list_lite( + self, client: PlaneClient, workspace_slug: str, project: Project + ) -> None: + """list_lite returns a paginated envelope of CycleLite items.""" + params = LiteListQueryParams(per_page=5, order_by="-created_at") + response = client.cycles.list_lite(workspace_slug, project.id, params=params) + assert isinstance(response.results, list) + assert isinstance(response.total_count, int) + assert isinstance(response.next_page_results, bool) + assert len(response.results) <= 5 + for cycle in response.results: + assert isinstance(cycle, CycleLite) + def test_list_archived_cycles( self, client: PlaneClient, workspace_slug: str, project: Project ) -> None: diff --git a/tests/unit/test_modules.py b/tests/unit/test_modules.py index cfeb0df..f174ce7 100644 --- a/tests/unit/test_modules.py +++ b/tests/unit/test_modules.py @@ -6,8 +6,9 @@ from plane.client import PlaneClient from plane.models.enums import ModuleStatus -from plane.models.modules import CreateModule, UpdateModule +from plane.models.modules import CreateModule, ModuleLite, UpdateModule from plane.models.projects import Project, ProjectFeature +from plane.models.query_params import LiteListQueryParams class TestModulesAPI: @@ -23,6 +24,19 @@ def test_list_modules( assert hasattr(response, "count") assert isinstance(response.results, list) + def test_list_lite( + self, client: PlaneClient, workspace_slug: str, project: Project + ) -> None: + """list_lite returns a paginated envelope of ModuleLite items.""" + params = LiteListQueryParams(per_page=5, order_by="-created_at") + response = client.modules.list_lite(workspace_slug, project.id, params=params) + assert isinstance(response.results, list) + assert isinstance(response.total_count, int) + assert isinstance(response.next_page_results, bool) + assert len(response.results) <= 5 + for module in response.results: + assert isinstance(module, ModuleLite) + def test_list_archived_modules( self, client: PlaneClient, workspace_slug: str, project: Project ) -> None: diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index e4d05b7..c21ce9c 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -5,8 +5,15 @@ import pytest from plane.client import PlaneClient -from plane.models.projects import CreateProject, Project, ProjectMember, UpdateProject +from plane.models.projects import ( + CreateProject, + Project, + ProjectLite, + ProjectMember, + UpdateProject, +) from plane.models.query_params import ( + LiteListQueryParams, MemberListQueryParams, MemberQueryParams, PaginatedQueryParams, @@ -32,6 +39,24 @@ def test_list_projects_with_params(self, client: PlaneClient, workspace_slug: st assert hasattr(response, "results") assert len(response.results) <= 5 + def test_list_lite(self, client: PlaneClient, workspace_slug: str) -> None: + """list_lite returns a paginated envelope of ProjectLite items.""" + response = client.projects.list_lite(workspace_slug) + assert isinstance(response.results, list) + assert isinstance(response.total_count, int) + assert isinstance(response.next_page_results, bool) + for project in response.results: + assert isinstance(project, ProjectLite) + assert project.name is not None + assert project.identifier is not None + + def test_list_lite_with_params(self, client: PlaneClient, workspace_slug: str) -> None: + """list_lite honors ordering + cursor pagination params.""" + params = LiteListQueryParams(per_page=5, order_by="-created_at") + response = client.projects.list_lite(workspace_slug, params=params) + assert isinstance(response.results, list) + assert len(response.results) <= 5 + class TestProjectsAPICRUD: """Test Projects API CRUD operations.""" From 82927feeb6d0434ca85061ccdb2086150607c028 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Mon, 29 Jun 2026 22:11:50 +0530 Subject: [PATCH 2/3] dev: updated project archived, cycle filter by view, and project role distribution --- README.md | 47 +++++++++++++++++++++++++++++++ plane/api/cycles.py | 19 ++++++++----- plane/api/modules.py | 4 +-- plane/api/projects.py | 25 +++++++++++------ plane/api/workspaces.py | 14 +++++++++ plane/models/__init__.py | 4 +++ plane/models/enums.py | 3 ++ plane/models/projects.py | 1 + plane/models/query_params.py | 53 +++++++++++++++++++++++++++++++++++ plane/models/workspaces.py | 28 ++++++++++++++++++ tests/unit/test_cycles.py | 15 ++++++++-- tests/unit/test_projects.py | 14 +++++++-- tests/unit/test_workspaces.py | 14 ++++++++- 13 files changed, 219 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index c2887cd..f125b77 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,13 @@ while paginated_members.next_page_results: params=MemberListQueryParams(per_page=1000, cursor=paginated_members.next_cursor), ) all_members.extend(paginated_members.results) + +# Project-role distribution — member counts per role across all active +# (non-archived) projects in the workspace (built-in + custom roles) +distribution = client.workspaces.get_project_role_distribution(workspace_slug) +print(distribution.total_memberships, distribution.total_distinct_members) +for role in distribution.roles: + print(role.slug, role.membership_count, role.distinct_member_count) ``` #### Roles @@ -372,6 +379,24 @@ members = client.projects.get_members_lite( workspace_slug, project_id, params=MemberListQueryParams(per_page=1000), ) + +# Paginated "lite" project list (id, identifier, name, icon/emoji, description, +# cover image, archived_at) — for pickers/reference lookups. +from plane.models.query_params import ProjectLiteListQueryParams + +lite = client.projects.list_lite( + workspace_slug, + params=ProjectLiteListQueryParams(per_page=1000, order_by="-created_at"), +) +for p in lite.results: + print(p.identifier, p.name) + +# NOTE: archived projects are now EXCLUDED by default. Pass include_archived=True +# to restore the previous behavior of listing archived projects too. +lite = client.projects.list_lite( + workspace_slug, + params=ProjectLiteListQueryParams(include_archived=True), +) ``` #### Work Items @@ -499,6 +524,18 @@ client.cycles.delete(workspace_slug, project_id, cycle_id) # List archived cycles archived = client.cycles.list_archived(workspace_slug, project_id) +# Paginated "lite" cycle list (full cycle fields minus issue-count metrics). +# Supports a cycle_view status filter: current | upcoming | completed | draft | +# incomplete (omit for all). Unlike the full list endpoint, this always paginates. +from plane.models.query_params import CycleLiteListQueryParams + +lite = client.cycles.list_lite( + workspace_slug, project_id, + params=CycleLiteListQueryParams(cycle_view="current", per_page=1000), +) +for c in lite.results: + print(c.name) + # Add work items to cycle from plane.models.cycles import AddWorkItemsToCycleRequest @@ -557,6 +594,16 @@ client.modules.delete(workspace_slug, project_id, module_id) # List archived modules archived = client.modules.list_archived(workspace_slug, project_id) +# Paginated "lite" module list (full module fields minus issue-count metrics) +from plane.models.query_params import LiteListQueryParams + +lite = client.modules.list_lite( + workspace_slug, project_id, + params=LiteListQueryParams(per_page=1000, order_by="-created_at"), +) +for m in lite.results: + print(m.name) + # Add work items to module from plane.models.modules import AddWorkItemsToModuleRequest diff --git a/plane/api/cycles.py b/plane/api/cycles.py index 4a6d857..bfb8658 100644 --- a/plane/api/cycles.py +++ b/plane/api/cycles.py @@ -11,7 +11,7 @@ TransferCycleWorkItemsRequest, UpdateCycle, ) -from ..models.query_params import LiteListQueryParams, WorkItemQueryParams +from ..models.query_params import CycleLiteListQueryParams, WorkItemQueryParams from .base_resource import BaseResource from .work_items.base import prepare_work_item_params @@ -89,24 +89,29 @@ def list_lite( self, workspace_slug: str, project_id: str, - params: LiteListQueryParams | None = None, + params: CycleLiteListQueryParams | None = None, ) -> PaginatedCycleLiteResponse: """List cycles as a paginated "lite" response. Calls the read-only ``/cycles-lite/`` endpoint, which returns the full cycle field set minus the issue-count metric annotations (total_issues, completed_issues, etc.), suitable for pickers and reference lookups. - Only ordering and cursor pagination are supported -- there are no field - filters. ``per_page`` defaults to and caps at 1000. + Supports ordering, cursor pagination, and a ``cycle_view`` status filter + -- there are no field filters. ``per_page`` defaults to and caps at 1000. + + Unlike the full ``cycles`` list endpoint (where ``cycle_view=current`` + returns a bare array), this endpoint always returns the paginated + envelope for every ``cycle_view`` value. Args: workspace_slug: The workspace slug identifier project_id: UUID of the project - params: Optional ordering + cursor pagination query parameters + params: Optional ordering + cursor pagination query parameters, + plus the ``cycle_view`` status filter """ - query_params = params.model_dump(exclude_none=True) if params else None response = self._get( - f"{workspace_slug}/projects/{project_id}/cycles-lite", params=query_params + f"{workspace_slug}/projects/{project_id}/cycles-lite", + params=params.to_query_params() if params else None, ) return PaginatedCycleLiteResponse.model_validate(response) diff --git a/plane/api/modules.py b/plane/api/modules.py index 463f6e0..1f6de88 100644 --- a/plane/api/modules.py +++ b/plane/api/modules.py @@ -103,9 +103,9 @@ def list_lite( project_id: UUID of the project params: Optional ordering + cursor pagination query parameters """ - query_params = params.model_dump(exclude_none=True) if params else None response = self._get( - f"{workspace_slug}/projects/{project_id}/modules-lite", params=query_params + f"{workspace_slug}/projects/{project_id}/modules-lite", + params=params.to_query_params() if params else None, ) return PaginatedModuleLiteResponse.model_validate(response) diff --git a/plane/api/projects.py b/plane/api/projects.py index 9dff63f..19c5755 100644 --- a/plane/api/projects.py +++ b/plane/api/projects.py @@ -15,10 +15,10 @@ UpdateProject, ) from ..models.query_params import ( - LiteListQueryParams, MemberListQueryParams, MemberQueryParams, PaginatedQueryParams, + ProjectLiteListQueryParams, ) from .base_resource import BaseResource @@ -83,22 +83,31 @@ def list( return PaginatedProjectResponse.model_validate(response) def list_lite( - self, workspace_slug: str, params: LiteListQueryParams | None = None + self, workspace_slug: str, params: ProjectLiteListQueryParams | None = None ) -> PaginatedProjectLiteResponse: """List projects as a paginated "lite" response. Calls the read-only ``/projects-lite/`` endpoint, which returns a field-trimmed shape (id, identifier, name, cover_image, icon_prop, - emoji, description, cover_image_url) suitable for pickers and reference - lookups. Only ordering and cursor pagination are supported -- there are - no field filters. ``per_page`` defaults to and caps at 1000. + emoji, description, cover_image_url, archived_at) suitable for pickers + and reference lookups. Supports ordering, cursor pagination, and an + ``include_archived`` toggle -- there are no field filters. ``per_page`` + defaults to and caps at 1000. + + .. note:: + Archived projects are now **excluded** by default. Pass + ``ProjectLiteListQueryParams(include_archived=True)`` to restore the + previous behavior of listing archived projects too. Args: workspace_slug: The workspace slug identifier - params: Optional ordering + cursor pagination query parameters + params: Optional ordering + cursor pagination query parameters, + plus the ``include_archived`` toggle """ - query_params = params.model_dump(exclude_none=True) if params else None - response = self._get(f"{workspace_slug}/projects-lite", params=query_params) + response = self._get( + f"{workspace_slug}/projects-lite", + params=params.to_query_params() if params else None, + ) return PaginatedProjectLiteResponse.model_validate(response) def get_worklog_summary(self, workspace_slug: str, project_id: str) -> [ProjectWorklogSummary]: diff --git a/plane/api/workspaces.py b/plane/api/workspaces.py index 3a4855e..3b5f9fc 100644 --- a/plane/api/workspaces.py +++ b/plane/api/workspaces.py @@ -5,6 +5,7 @@ from ..models.query_params import MemberListQueryParams, MemberQueryParams from ..models.workspaces import ( PaginatedWorkspaceMemberResponse, + ProjectRoleDistribution, WorkspaceFeature, WorkspaceMember, ) @@ -54,6 +55,19 @@ def get_members_lite( ) return PaginatedWorkspaceMemberResponse.model_validate(response) + def get_project_role_distribution(self, workspace_slug: str) -> ProjectRoleDistribution: + """Get the distribution of project members by role across the workspace. + + Aggregates member counts per role over all active (non-archived) + projects in the workspace. Both built-in roles (admin, contributor, + commenter, guest) and custom roles are included. + + Args: + workspace_slug: The workspace slug identifier + """ + response = self._get(f"{workspace_slug}/project-role-distribution") + return ProjectRoleDistribution.model_validate(response) + def get_features(self, workspace_slug: str) -> WorkspaceFeature: """Get features of a workspace. diff --git a/plane/models/__init__.py b/plane/models/__init__.py index c53e77a..951a5b3 100644 --- a/plane/models/__init__.py +++ b/plane/models/__init__.py @@ -15,10 +15,12 @@ ) from .query_params import ( BaseQueryParams, + CycleLiteListQueryParams, LiteListQueryParams, MemberListQueryParams, MemberQueryParams, PaginatedQueryParams, + ProjectLiteListQueryParams, RetrieveQueryParams, WorkItemQueryParams, ) @@ -40,10 +42,12 @@ "IntakeWorkItemStatusEnum", # query params "BaseQueryParams", + "CycleLiteListQueryParams", "LiteListQueryParams", "MemberListQueryParams", "MemberQueryParams", "PaginatedQueryParams", + "ProjectLiteListQueryParams", "RetrieveQueryParams", "WorkItemQueryParams", ] diff --git a/plane/models/enums.py b/plane/models/enums.py index 6b61a51..afe67dc 100644 --- a/plane/models/enums.py +++ b/plane/models/enums.py @@ -48,6 +48,7 @@ "FORMULA", ] RelationTypeEnum = Literal["ISSUE", "USER", "RELEASE"] +CycleViewEnum = Literal["current", "upcoming", "completed", "draft", "incomplete"] # Proper Enum classes for better type safety and IDE support @@ -93,6 +94,7 @@ class InitiativeState(Enum): COMPLETED = "COMPLETED" CLOSED = "CLOSED" + class WorkItemRelationType(Enum): """Work item relation type enumeration.""" @@ -580,6 +582,7 @@ class Group(Enum): "PriorityEnum", "PropertyTypeEnum", "RelationTypeEnum", + "CycleViewEnum", "TimezoneEnum", "TypeMimeEnum", "NetworkEnum", diff --git a/plane/models/projects.py b/plane/models/projects.py index e399fe6..870ca10 100644 --- a/plane/models/projects.py +++ b/plane/models/projects.py @@ -75,6 +75,7 @@ class ProjectLite(BaseModel): emoji: str | None = None description: str | None = None cover_image_url: str | None = None + archived_at: str | None = None class CreateProject(BaseModel): diff --git a/plane/models/query_params.py b/plane/models/query_params.py index 4e5b02a..8adc7de 100644 --- a/plane/models/query_params.py +++ b/plane/models/query_params.py @@ -4,6 +4,8 @@ from pydantic import BaseModel, ConfigDict, Field +from .enums import CycleViewEnum + class BaseQueryParams(BaseModel): """Base query parameters for API requests.""" @@ -180,6 +182,55 @@ class LiteListQueryParams(BaseModel): description="Field to order results by. Prefix with '-' for descending order", ) + def to_query_params(self) -> dict[str, Any]: + """Serialize to a query-param dict the lite endpoints accept. + + Booleans are rendered as lowercase ``"true"``/``"false"`` strings so the + backend parses them (a Python ``True`` would be encoded as ``"True"`` and + rejected). Unset fields are dropped so they never reach the query string. + """ + raw = self.model_dump(exclude_none=True) + return {k: (str(v).lower() if isinstance(v, bool) else v) for k, v in raw.items()} + + +class ProjectLiteListQueryParams(LiteListQueryParams): + """Query parameters for the projects-lite list endpoint. + + Adds the ``include_archived`` toggle to the shared lite ordering + cursor + pagination params. + """ + + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + include_archived: bool | None = Field( + None, + description=( + "Include archived projects in the results. Defaults to False on the " + "server, which excludes archived projects. Set True to restore the " + "previous behavior of listing archived projects too." + ), + ) + + +class CycleLiteListQueryParams(LiteListQueryParams): + """Query parameters for the cycles-lite list endpoint. + + Adds the ``cycle_view`` status filter to the shared lite ordering + cursor + pagination params. + """ + + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + cycle_view: CycleViewEnum | None = Field( + None, + description=( + "Filter cycles by status: 'current' (started, not yet ended), " + "'upcoming' (starts in the future), 'completed' (ended), 'draft' " + "(no start/end dates), or 'incomplete' (not yet finished or " + "open-ended). Omit to return all cycles." + ), + ) + WorkItemCountGroupBy = Literal[ "state_id", @@ -247,9 +298,11 @@ class WorkItemCountQueryParams(BaseModel): __all__ = [ "BaseQueryParams", + "CycleLiteListQueryParams", "LiteListQueryParams", "MemberListQueryParams", "MemberQueryParams", + "ProjectLiteListQueryParams", "PaginatedQueryParams", "RetrieveQueryParams", "WorkItemCountGroupBy", diff --git a/plane/models/workspaces.py b/plane/models/workspaces.py index 94b554b..89247e6 100644 --- a/plane/models/workspaces.py +++ b/plane/models/workspaces.py @@ -37,3 +37,31 @@ class WorkspaceFeature(BaseModel): customers: bool wiki: bool pi: bool + + +class ProjectRoleDistributionEntry(BaseModel): + """Per-role membership counts within a workspace's project-role distribution.""" + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + role_id: str | None = None + name: str | None = None + slug: str | None = None + is_system: bool | None = None + level: int | None = None + membership_count: int | None = None + distinct_member_count: int | None = None + + +class ProjectRoleDistribution(BaseModel): + """Aggregate count of project members by role across a workspace. + + Counts span all active (non-archived) projects in the workspace and include + both built-in roles (admin, contributor, commenter, guest) and custom roles. + """ + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + total_memberships: int | None = None + total_distinct_members: int | None = None + roles: list[ProjectRoleDistributionEntry] = [] diff --git a/tests/unit/test_cycles.py b/tests/unit/test_cycles.py index a02ee0e..6620455 100644 --- a/tests/unit/test_cycles.py +++ b/tests/unit/test_cycles.py @@ -7,7 +7,7 @@ from plane.client import PlaneClient from plane.models.cycles import CreateCycle, CycleLite, UpdateCycle from plane.models.projects import Project, ProjectFeature -from plane.models.query_params import LiteListQueryParams +from plane.models.query_params import CycleLiteListQueryParams class TestCyclesAPI: @@ -27,7 +27,7 @@ def test_list_lite( self, client: PlaneClient, workspace_slug: str, project: Project ) -> None: """list_lite returns a paginated envelope of CycleLite items.""" - params = LiteListQueryParams(per_page=5, order_by="-created_at") + params = CycleLiteListQueryParams(per_page=5, order_by="-created_at") response = client.cycles.list_lite(workspace_slug, project.id, params=params) assert isinstance(response.results, list) assert isinstance(response.total_count, int) @@ -36,6 +36,17 @@ def test_list_lite( for cycle in response.results: assert isinstance(cycle, CycleLite) + def test_list_lite_cycle_view( + self, client: PlaneClient, workspace_slug: str, project: Project + ) -> None: + """list_lite honors the cycle_view status filter and stays paginated.""" + params = CycleLiteListQueryParams(cycle_view="current", per_page=5) + response = client.cycles.list_lite(workspace_slug, project.id, params=params) + assert isinstance(response.results, list) + assert isinstance(response.total_count, int) + for cycle in response.results: + assert isinstance(cycle, CycleLite) + def test_list_archived_cycles( self, client: PlaneClient, workspace_slug: str, project: Project ) -> None: diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index c21ce9c..04dfbd9 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -13,10 +13,10 @@ UpdateProject, ) from plane.models.query_params import ( - LiteListQueryParams, MemberListQueryParams, MemberQueryParams, PaginatedQueryParams, + ProjectLiteListQueryParams, ) @@ -52,11 +52,21 @@ def test_list_lite(self, client: PlaneClient, workspace_slug: str) -> None: def test_list_lite_with_params(self, client: PlaneClient, workspace_slug: str) -> None: """list_lite honors ordering + cursor pagination params.""" - params = LiteListQueryParams(per_page=5, order_by="-created_at") + params = ProjectLiteListQueryParams(per_page=5, order_by="-created_at") response = client.projects.list_lite(workspace_slug, params=params) assert isinstance(response.results, list) assert len(response.results) <= 5 + def test_list_lite_include_archived(self, client: PlaneClient, workspace_slug: str) -> None: + """list_lite with include_archived=True returns a valid envelope. + + Archived projects are excluded by default; this opt-in restores them. + """ + params = ProjectLiteListQueryParams(include_archived=True, per_page=5) + response = client.projects.list_lite(workspace_slug, params=params) + assert isinstance(response.results, list) + assert isinstance(response.total_count, int) + class TestProjectsAPICRUD: """Test Projects API CRUD operations.""" diff --git a/tests/unit/test_workspaces.py b/tests/unit/test_workspaces.py index dd4d41d..e787209 100644 --- a/tests/unit/test_workspaces.py +++ b/tests/unit/test_workspaces.py @@ -2,7 +2,7 @@ from plane.client import PlaneClient from plane.models.query_params import MemberListQueryParams, MemberQueryParams -from plane.models.workspaces import WorkspaceMember +from plane.models.workspaces import ProjectRoleDistributionEntry, WorkspaceMember class TestWorkspacesAPI: @@ -57,6 +57,18 @@ def test_get_members_lite_paginated(self, client: PlaneClient, workspace_slug: s for member in paginated_members.results: assert isinstance(member, WorkspaceMember) + def test_get_project_role_distribution(self, client: PlaneClient, workspace_slug: str) -> None: + """get_project_role_distribution returns aggregate role counts.""" + distribution = client.workspaces.get_project_role_distribution(workspace_slug) + assert isinstance(distribution.total_memberships, int) + assert isinstance(distribution.total_distinct_members, int) + assert isinstance(distribution.roles, list) + for entry in distribution.roles: + assert isinstance(entry, ProjectRoleDistributionEntry) + assert hasattr(entry, "slug") + assert hasattr(entry, "membership_count") + assert hasattr(entry, "distinct_member_count") + def test_get_features(self, client: PlaneClient, workspace_slug: str) -> None: """Test getting workspace features.""" features = client.workspaces.get_features(workspace_slug) From ac6634b4582c5865e9806e8dd04cb907397990ed Mon Sep 17 00:00:00 2001 From: akhil-vamshi-konam Date: Mon, 29 Jun 2026 23:20:10 +0530 Subject: [PATCH 3/3] chore: bump sdk version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0178cbc..ee42cd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "plane-sdk" -version = "0.2.18" +version = "0.2.19" description = "Python SDK for Plane API" readme = "README.md" requires-python = ">=3.10"