Skip to content

Commit a789ece

Browse files
authored
feat: add collections resource (#64)
* feat: add Collections resource client Adds a full Collections resource (create/list/retrieve/update/delete collections, add/list/search/move/remove pages within a collection, and collection member CRUD) matching the new external Collections API, plus `collection_id`/`parent_id` on CreatePage so pages can be created directly inside a collection or as a sub-page. * fix(tests): assert sub-page via parent_id filter — unfiltered collection listing returns root-branch pages only * bumpup version to 0.2.21 * resolve review comments * resolve coderabbit comments * test: drop unsupported sub-page nesting assertions Sub-page creation is not supported by the public API (CreatePage.parent_id is accepted but not honored on creation), so the test now covers only the supported page-in-collection behavior.
1 parent 78702e9 commit a789ece

10 files changed

Lines changed: 656 additions & 2 deletions

File tree

plane/api/collections/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .base import Collections
2+
3+
__all__ = ["Collections"]

plane/api/collections/base.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from plane.api.base_resource import BaseResource
6+
from plane.api.collections.members import CollectionMembers
7+
from plane.api.collections.pages import CollectionPages
8+
from plane.models.collections import (
9+
Collection,
10+
CreateCollection,
11+
UpdateCollection,
12+
)
13+
14+
15+
class Collections(BaseResource):
16+
def __init__(self, config: Any) -> None:
17+
super().__init__(config, "/workspaces/")
18+
19+
# Initialize sub-resources
20+
self.pages = CollectionPages(config)
21+
self.members = CollectionMembers(config)
22+
23+
def list(self, workspace_slug: str) -> list[Collection]:
24+
"""List all collections in a workspace.
25+
26+
Args:
27+
workspace_slug: The workspace slug identifier
28+
"""
29+
response = self._get(f"{workspace_slug}/collections")
30+
return [Collection.model_validate(item) for item in response]
31+
32+
def create(self, workspace_slug: str, data: CreateCollection) -> Collection:
33+
"""Create a new collection in a workspace.
34+
35+
Args:
36+
workspace_slug: The workspace slug identifier
37+
data: Collection data
38+
"""
39+
response = self._post(f"{workspace_slug}/collections", data.model_dump(exclude_none=True))
40+
return Collection.model_validate(response)
41+
42+
def retrieve(self, workspace_slug: str, collection_id: str) -> Collection:
43+
"""Retrieve a collection by ID.
44+
45+
Args:
46+
workspace_slug: The workspace slug identifier
47+
collection_id: UUID of the collection
48+
"""
49+
response = self._get(f"{workspace_slug}/collections/{collection_id}")
50+
return Collection.model_validate(response)
51+
52+
def update(self, workspace_slug: str, collection_id: str, data: UpdateCollection) -> Collection:
53+
"""Update a collection's name, logo, or sort order.
54+
55+
Args:
56+
workspace_slug: The workspace slug identifier
57+
collection_id: UUID of the collection
58+
data: Fields to update (access cannot be changed after creation)
59+
"""
60+
response = self._patch(
61+
f"{workspace_slug}/collections/{collection_id}",
62+
data.model_dump(exclude_none=True),
63+
)
64+
return Collection.model_validate(response)
65+
66+
def delete(
67+
self,
68+
workspace_slug: str,
69+
collection_id: str,
70+
archive_pages: bool | None = None,
71+
) -> None:
72+
"""Delete a collection.
73+
74+
Args:
75+
workspace_slug: The workspace slug identifier
76+
collection_id: UUID of the collection
77+
archive_pages: Whether to archive the collection's pages instead of
78+
leaving them unfiled. Omit to use the server's default (True).
79+
Private collections always archive their pages regardless.
80+
"""
81+
params = None
82+
if archive_pages is not None:
83+
params = {"archive_pages": "true" if archive_pages else "false"}
84+
return self._delete(f"{workspace_slug}/collections/{collection_id}", params=params)

plane/api/collections/members.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from plane.api.base_resource import BaseResource
6+
from plane.models.collections import (
7+
CollectionMember,
8+
CreateCollectionMember,
9+
UpdateCollectionMember,
10+
)
11+
12+
13+
class CollectionMembers(BaseResource):
14+
def __init__(self, config: Any) -> None:
15+
super().__init__(config, "/workspaces/")
16+
17+
def list(self, workspace_slug: str, collection_id: str) -> list[CollectionMember]:
18+
"""List members of a (typically private) collection.
19+
20+
Args:
21+
workspace_slug: The workspace slug identifier
22+
collection_id: UUID of the collection
23+
"""
24+
response = self._get(f"{workspace_slug}/collections/{collection_id}/members")
25+
return [CollectionMember.model_validate(item) for item in response]
26+
27+
def add(
28+
self, workspace_slug: str, collection_id: str, data: CreateCollectionMember
29+
) -> CollectionMember:
30+
"""Add a member to a collection.
31+
32+
Args:
33+
workspace_slug: The workspace slug identifier
34+
collection_id: UUID of the collection
35+
data: Member user id and access level
36+
"""
37+
response = self._post(
38+
f"{workspace_slug}/collections/{collection_id}/members",
39+
data.model_dump(exclude_none=True),
40+
)
41+
return CollectionMember.model_validate(response)
42+
43+
def update(
44+
self,
45+
workspace_slug: str,
46+
collection_id: str,
47+
member_id: str,
48+
data: UpdateCollectionMember,
49+
) -> CollectionMember:
50+
"""Update a collection member's access level.
51+
52+
Args:
53+
workspace_slug: The workspace slug identifier
54+
collection_id: UUID of the collection
55+
member_id: UUID of the CollectionMember row (not the user id)
56+
data: New access level
57+
"""
58+
response = self._patch(
59+
f"{workspace_slug}/collections/{collection_id}/members/{member_id}",
60+
data.model_dump(exclude_none=True),
61+
)
62+
return CollectionMember.model_validate(response)
63+
64+
def remove(self, workspace_slug: str, collection_id: str, member_id: str) -> None:
65+
"""Remove a member from a collection.
66+
67+
Args:
68+
workspace_slug: The workspace slug identifier
69+
collection_id: UUID of the collection
70+
member_id: UUID of the CollectionMember row (not the user id)
71+
"""
72+
return self._delete(f"{workspace_slug}/collections/{collection_id}/members/{member_id}")

plane/api/collections/pages.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from plane.api.base_resource import BaseResource
6+
from plane.models.collections import (
7+
AddCollectionPages,
8+
CollectionPage,
9+
CollectionPageSearchResult,
10+
PaginatedCollectionPageResponse,
11+
UpdateCollectionPage,
12+
)
13+
from plane.models.query_params import CollectionPageQueryParams
14+
15+
16+
class CollectionPages(BaseResource):
17+
def __init__(self, config: Any) -> None:
18+
super().__init__(config, "/workspaces/")
19+
20+
def list(
21+
self,
22+
workspace_slug: str,
23+
collection_id: str,
24+
params: CollectionPageQueryParams | None = None,
25+
) -> PaginatedCollectionPageResponse:
26+
"""List pages that belong to a collection.
27+
28+
Args:
29+
workspace_slug: The workspace slug identifier
30+
collection_id: UUID of the collection
31+
params: Optional search/parent_id/pagination filters
32+
"""
33+
query_params = params.model_dump(exclude_none=True) if params else None
34+
response = self._get(
35+
f"{workspace_slug}/collections/{collection_id}/pages", params=query_params
36+
)
37+
return PaginatedCollectionPageResponse.model_validate(response)
38+
39+
def add(
40+
self, workspace_slug: str, collection_id: str, data: AddCollectionPages
41+
) -> list[CollectionPage]:
42+
"""Add existing page(s) to a collection.
43+
44+
Args:
45+
workspace_slug: The workspace slug identifier
46+
collection_id: UUID of the collection
47+
data: Page IDs to add, with optional sort_orders/placement
48+
"""
49+
response = self._post(
50+
f"{workspace_slug}/collections/{collection_id}/pages",
51+
data.model_dump(exclude_none=True),
52+
)
53+
return [CollectionPage.model_validate(item) for item in response]
54+
55+
def search(
56+
self, workspace_slug: str, collection_id: str, search: str | None = None
57+
) -> list[CollectionPageSearchResult]:
58+
"""Search pages that are not yet in a collection, to add them.
59+
60+
Args:
61+
workspace_slug: The workspace slug identifier
62+
collection_id: UUID of the collection
63+
search: Optional case-insensitive substring filter on page name
64+
"""
65+
query_params = {"search": search} if search else None
66+
response = self._get(
67+
f"{workspace_slug}/collections/{collection_id}/pages-search",
68+
params=query_params,
69+
)
70+
return [CollectionPageSearchResult.model_validate(item) for item in response]
71+
72+
def update(
73+
self,
74+
workspace_slug: str,
75+
collection_id: str,
76+
page_collection_id: str,
77+
data: UpdateCollectionPage,
78+
) -> CollectionPage:
79+
"""Move a page to a different collection, or reorder it within the current one.
80+
81+
Args:
82+
workspace_slug: The workspace slug identifier
83+
collection_id: UUID of the page's current collection
84+
page_collection_id: UUID of the page-collection membership row
85+
data: `collection` to move (omit/leave unset to just reorder),
86+
and/or `sort_order`/`placement` to reorder
87+
"""
88+
response = self._patch(
89+
f"{workspace_slug}/collections/{collection_id}/pages/{page_collection_id}",
90+
data.model_dump(exclude_none=True),
91+
)
92+
return CollectionPage.model_validate(response)
93+
94+
def remove(self, workspace_slug: str, collection_id: str, page_collection_id: str) -> None:
95+
"""Remove a page from a collection (does not delete the page itself).
96+
97+
Args:
98+
workspace_slug: The workspace slug identifier
99+
collection_id: UUID of the collection
100+
page_collection_id: UUID of the page-collection membership row
101+
"""
102+
return self._delete(
103+
f"{workspace_slug}/collections/{collection_id}/pages/{page_collection_id}"
104+
)

plane/client/plane_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from ..api.agent_runs import AgentRuns
2+
from ..api.collections import Collections
23
from ..api.customers import Customers
34
from ..api.cycles import Cycles
45
from ..api.epics import Epics
@@ -61,6 +62,7 @@ def __init__(
6162
self.epics = Epics(self.config)
6263
self.work_items = WorkItems(self.config)
6364
self.pages = Pages(self.config)
65+
self.collections = Collections(self.config)
6466
self.labels = Labels(self.config)
6567
self.states = States(self.config)
6668
self.milestones = Milestones(self.config)

0 commit comments

Comments
 (0)