feat: Add member filtering support to workspace and project member list endpoints#53
Conversation
…e and project members
|
Warning Review limit reached
More reviews will be available in 42 minutes and 45 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe PR adds typed member query parameters, extends workspace and project member schemas, updates member-list endpoints to use them, and refreshes tests, README examples, and package metadata. ChangesMember filtering and pagination
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
plane/api/workspaces.py (1)
15-29: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Workspaces.get_members()no longer accepts raw dict params.Line 28 assumes any truthy
paramshasmodel_dump(), so an existing call likeget_members(..., params={"is_active": True})now raisesAttributeError. That breaks the PR's stated backward-compatible dict support and makes workspaces inconsistent withProjects.get_members().Suggested fix
+from collections.abc import Mapping from typing import Any @@ - def get_members( - self, workspace_slug: str, params: MemberQueryParams | None = None - ) -> list[WorkspaceMember]: + def get_members( + self, + workspace_slug: str, + params: MemberQueryParams | Mapping[str, Any] | None = None, + ) -> list[WorkspaceMember]: @@ - response = self._get( - f"{workspace_slug}/members", - params=params.model_dump(exclude_none=True) if params else None, - ) + if isinstance(params, MemberQueryParams): + params = params.model_dump(exclude_none=True) + elif params is not None: + params = {key: value for key, value in params.items() if value not in (None, "")} + + response = self._get(f"{workspace_slug}/members/", params=params)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plane/api/workspaces.py` around lines 15 - 29, Workspaces.get_members() currently assumes params is always a MemberQueryParams instance and calls model_dump(), which breaks existing raw dict callers with AttributeError. Update the Workspaces.get_members method to accept both MemberQueryParams and dict inputs, matching the behavior of Projects.get_members(). Normalize params before passing it to self._get so dicts are forwarded directly and model_dump() is only used when params is a model instance.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@plane/api/projects.py`:
- Around line 102-107: The members query in the project API still forwards empty
filter values when params is a raw mapping, so the documented “empty filters are
omitted” behavior is only applied in the MemberQueryParams path. Update the
normalization in the projects members request flow so both MemberQueryParams and
mapping inputs are cleaned before calling _get, using the existing params
handling in the projects API method to drop empty string/empty-like values as
well as None. Keep the fix localized around the member listing method that
builds the /members request.
In `@plane/api/workspaces.py`:
- Around line 26-28: The workspace members request is still using the
non-trailing-slash path, so update the endpoint in the members fetch logic to
use the trailing slash form. In the method that builds the `_get` call for
workspace members, change the `f"{workspace_slug}/members"` target to the
trailing-slash variant so it matches the API path convention used across the
client.
In `@plane/models/query_params.py`:
- Around line 93-123: The blank-string filters in MemberQueryParams are still
being serialized because only None is excluded, so empty values like
display_name="" leak into member-list requests. Update the MemberQueryParams
handling so empty strings are treated as unset and omitted during serialization,
likely by normalizing blank text fields to None in the model or by adding a
field-level serializer/validator on the text filter fields (first_name,
last_name, email, display_name). Ensure the behavior matches the
BaseQueryParams/member-list contract that empty filters are not sent.
---
Outside diff comments:
In `@plane/api/workspaces.py`:
- Around line 15-29: Workspaces.get_members() currently assumes params is always
a MemberQueryParams instance and calls model_dump(), which breaks existing raw
dict callers with AttributeError. Update the Workspaces.get_members method to
accept both MemberQueryParams and dict inputs, matching the behavior of
Projects.get_members(). Normalize params before passing it to self._get so dicts
are forwarded directly and model_dump() is only used when params is a model
instance.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b5d71c47-7b16-48f9-b86c-8c26570d4744
📒 Files selected for processing (8)
plane/api/projects.pyplane/api/workspaces.pyplane/models/__init__.pyplane/models/projects.pyplane/models/query_params.pyplane/models/workspaces.pytests/unit/test_projects.pytests/unit/test_workspaces.py
| params: Optional query parameters. Accepts a ``MemberQueryParams`` | ||
| instance for typed filtering/ordering, or a raw mapping. | ||
| """ | ||
| if isinstance(params, MemberQueryParams): | ||
| params = params.model_dump(exclude_none=True) | ||
| response = self._get(f"{workspace_slug}/projects/{project_id}/members", params=params) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Backcompat mappings still forward empty filters.
Only the MemberQueryParams branch is normalized here, and even that only drops None. A raw mapping like {"display_name": ""} is forwarded straight to _get, so the backcompat path does not match the documented "empty filters are omitted" behavior.
Suggested fix
if isinstance(params, MemberQueryParams):
params = params.model_dump(exclude_none=True)
+ if params is not None:
+ params = {key: value for key, value in params.items() if value not in (None, "")}
response = self._get(f"{workspace_slug}/projects/{project_id}/members", params=params)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| params: Optional query parameters. Accepts a ``MemberQueryParams`` | |
| instance for typed filtering/ordering, or a raw mapping. | |
| """ | |
| if isinstance(params, MemberQueryParams): | |
| params = params.model_dump(exclude_none=True) | |
| response = self._get(f"{workspace_slug}/projects/{project_id}/members", params=params) | |
| params: Optional query parameters. Accepts a ``MemberQueryParams`` | |
| instance for typed filtering/ordering, or a raw mapping. | |
| """ | |
| if isinstance(params, MemberQueryParams): | |
| params = params.model_dump(exclude_none=True) | |
| if params is not None: | |
| params = {key: value for key, value in params.items() if value not in (None, "")} | |
| response = self._get(f"{workspace_slug}/projects/{project_id}/members", params=params) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plane/api/projects.py` around lines 102 - 107, The members query in the
project API still forwards empty filter values when params is a raw mapping, so
the documented “empty filters are omitted” behavior is only applied in the
MemberQueryParams path. Update the normalization in the projects members request
flow so both MemberQueryParams and mapping inputs are cleaned before calling
_get, using the existing params handling in the projects API method to drop
empty string/empty-like values as well as None. Keep the fix localized around
the member listing method that builds the /members request.
| response = self._get( | ||
| f"{workspace_slug}/members", | ||
| params=params.model_dump(exclude_none=True) if params else None, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use the trailing-slash member endpoint.
This changed request still targets f"{workspace_slug}/members" instead of the required trailing-slash form. As per path instructions, all API endpoints should end with a trailing /.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plane/api/workspaces.py` around lines 26 - 28, The workspace members request
is still using the non-trailing-slash path, so update the endpoint in the
members fetch logic to use the trailing slash form. In the method that builds
the `_get` call for workspace members, change the `f"{workspace_slug}/members"`
target to the trailing-slash variant so it matches the API path convention used
across the client.
Source: Path instructions
| class MemberQueryParams(BaseQueryParams): | ||
| """Query parameters for workspace/project member list endpoints. | ||
|
|
||
| Inherits the documented query parameters from BaseQueryParams (expand, | ||
| fields, external_id, external_source, order_by) and adds member-specific | ||
| filters. Text filters match case-insensitively on a substring; ``role_slug`` | ||
| matches exactly. Boolean filters narrow by membership/account flags. | ||
| """ | ||
|
|
||
| model_config = ConfigDict(extra="ignore", populate_by_name=True) | ||
|
|
||
| # text filters | ||
| first_name: str | None = Field( | ||
| None, description="Filter by member first name (case-insensitive contains)" | ||
| ) | ||
| last_name: str | None = Field( | ||
| None, description="Filter by member last name (case-insensitive contains)" | ||
| ) | ||
| email: str | None = Field( | ||
| None, description="Filter by member email (case-insensitive contains)" | ||
| ) | ||
| display_name: str | None = Field( | ||
| None, description="Filter by member display name (case-insensitive contains)" | ||
| ) | ||
| role_slug: str | None = Field(None, description="Filter by role slug (exact match)") | ||
|
|
||
| # boolean filters | ||
| is_active: bool | None = Field(None, description="Filter by active membership status") | ||
| is_bot: bool | None = Field(None, description="Filter by bot accounts") | ||
|
|
||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Blank string filters will still be sent.
model_dump(exclude_none=True) only drops None, so MemberQueryParams(display_name="") still serializes as display_name=. That contradicts the PR contract that empty filters are omitted and will leak blank filters into both typed member-list endpoints.
Suggested fix
+from pydantic import ConfigDict, Field, field_validator
+
class MemberQueryParams(BaseQueryParams):
@@
role_slug: str | None = Field(None, description="Filter by role slug (exact match)")
@@
is_active: bool | None = Field(None, description="Filter by active membership status")
is_bot: bool | None = Field(None, description="Filter by bot accounts")
+
+ `@field_validator`("first_name", "last_name", "email", "display_name", "role_slug", mode="before")
+ `@classmethod`
+ def _empty_strings_to_none(cls, value: str | None) -> str | None:
+ if isinstance(value, str) and not value.strip():
+ return None
+ return value📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| class MemberQueryParams(BaseQueryParams): | |
| """Query parameters for workspace/project member list endpoints. | |
| Inherits the documented query parameters from BaseQueryParams (expand, | |
| fields, external_id, external_source, order_by) and adds member-specific | |
| filters. Text filters match case-insensitively on a substring; ``role_slug`` | |
| matches exactly. Boolean filters narrow by membership/account flags. | |
| """ | |
| model_config = ConfigDict(extra="ignore", populate_by_name=True) | |
| # text filters | |
| first_name: str | None = Field( | |
| None, description="Filter by member first name (case-insensitive contains)" | |
| ) | |
| last_name: str | None = Field( | |
| None, description="Filter by member last name (case-insensitive contains)" | |
| ) | |
| email: str | None = Field( | |
| None, description="Filter by member email (case-insensitive contains)" | |
| ) | |
| display_name: str | None = Field( | |
| None, description="Filter by member display name (case-insensitive contains)" | |
| ) | |
| role_slug: str | None = Field(None, description="Filter by role slug (exact match)") | |
| # boolean filters | |
| is_active: bool | None = Field(None, description="Filter by active membership status") | |
| is_bot: bool | None = Field(None, description="Filter by bot accounts") | |
| from pydantic import ConfigDict, Field, field_validator | |
| class MemberQueryParams(BaseQueryParams): | |
| """Query parameters for workspace/project member list endpoints. | |
| Inherits the documented query parameters from BaseQueryParams (expand, | |
| fields, external_id, external_source, order_by) and adds member-specific | |
| filters. Text filters match case-insensitively on a substring; ``role_slug`` | |
| matches exactly. Boolean filters narrow by membership/account flags. | |
| """ | |
| model_config = ConfigDict(extra="ignore", populate_by_name=True) | |
| # text filters | |
| first_name: str | None = Field( | |
| None, description="Filter by member first name (case-insensitive contains)" | |
| ) | |
| last_name: str | None = Field( | |
| None, description="Filter by member last name (case-insensitive contains)" | |
| ) | |
| email: str | None = Field( | |
| None, description="Filter by member email (case-insensitive contains)" | |
| ) | |
| display_name: str | None = Field( | |
| None, description="Filter by member display name (case-insensitive contains)" | |
| ) | |
| role_slug: str | None = Field(None, description="Filter by role slug (exact match)") | |
| # boolean filters | |
| is_active: bool | None = Field(None, description="Filter by active membership status") | |
| is_bot: bool | None = Field(None, description="Filter by bot accounts") | |
| `@field_validator`("first_name", "last_name", "email", "display_name", "role_slug", mode="before") | |
| `@classmethod` | |
| def _empty_strings_to_none(cls, value: str | None) -> str | None: | |
| if isinstance(value, str) and not value.strip(): | |
| return None | |
| return value |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plane/models/query_params.py` around lines 93 - 123, The blank-string filters
in MemberQueryParams are still being serialized because only None is excluded,
so empty values like display_name="" leak into member-list requests. Update the
MemberQueryParams handling so empty strings are treated as unset and omitted
during serialization, likely by normalizing blank text fields to None in the
model or by adding a field-level serializer/validator on the text filter fields
(first_name, last_name, email, display_name). Ensure the behavior matches the
BaseQueryParams/member-list contract that empty filters are not sent.
…s in the existing and handled the pagination for new lite endpoints
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
plane/api/workspaces.py (1)
31-33: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winUse the trailing-slash member endpoints here.
These changed endpoints still omit the terminal
/, which violates the API path convention and can fail against routers that do not normalize slashless paths. As per path instructions,All API endpoints should end with a trailing / and follow URL convention: {base_path}/api/v1{resource_base_path}/{endpoint}/.Also applies to: 51-53
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plane/api/workspaces.py` around lines 31 - 33, The member-list endpoints in the workspace API calls are missing the required trailing slash, which can break routing on slash-sensitive servers. Update the request path built in the workspace members helper to use the trailing-slash form, and apply the same fix to the other related members call referenced in this review so the endpoint path consistently follows the API convention. Look for the self._get usages in the workspace members methods and ensure the final URL segment ends with /.Source: Path instructions
plane/models/query_params.py (1)
123-132: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winBlank text filters still leak into the query string.
model_dump(exclude_none=True)only dropsNone, so values likedisplay_name=""or" "still serialize and get sent by both member-list APIs. That breaks the PR contract that empty filters are omitted.Suggested fix
def to_query_params(self) -> dict[str, Any]: """Serialize to a query-param dict the member endpoints accept. @@ - raw = self.model_dump(exclude_none=True) + raw = { + k: v + for k, v in self.model_dump(exclude_none=True).items() + if not (isinstance(v, str) and not v.strip()) + } return {k: (str(v).lower() if isinstance(v, bool) else v) for k, v in raw.items()}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plane/models/query_params.py` around lines 123 - 132, The to_query_params method in QueryParams still serializes empty string filters because model_dump(exclude_none=True) only removes None values. Update the serialization logic to also drop blank text values like "" and whitespace-only strings before building the query-param dict, while preserving the existing boolean lowercasing behavior for accepted fields.plane/api/projects.py (1)
112-119: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRaw mapping filters still forward empty values.
The backcompat branch lowercases bools, but it still passes
Noneand blank strings straight through to_get(...). That means{"display_name": ""}does not honor the documented “empty filters are omitted” behavior.Suggested fix
if isinstance(params, MemberQueryParams): params = params.to_query_params() elif params is not None: # Lowercase bools so the typed filter backend accepts them # (requests would otherwise encode True as "True" -> HTTP 400). - params = {k: (str(v).lower() if isinstance(v, bool) else v) for k, v in params.items()} + params = { + k: (str(v).lower() if isinstance(v, bool) else v) + for k, v in params.items() + if v is not None and not (isinstance(v, str) and not v.strip()) + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@plane/api/projects.py` around lines 112 - 119, Empty mapping filter values are still being sent through in the project members query path. Update the params handling in the project-members request flow around the MemberQueryParams / raw mapping branch so blank strings and None are filtered out before calling _get(...), while still lowercasing bools for the typed filter backend. Keep the behavior consistent with the existing query param conversion helper used by the project member listing logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@plane/api/projects.py`:
- Around line 118-119: The project member endpoints in the `Projects` API are
missing the required trailing slash, which can cause route mismatches on
stricter servers. Update the `_get` calls for the `project-members` and related
member endpoint in `plane/api/projects.py` to use the conventionally formatted
path ending in `/`, matching the docstring and the API URL pattern used
throughout `plane/api/**/*.py`.
In `@plane/api/workspaces.py`:
- Around line 18-35: Workspaces.get_members() should preserve the raw-dictionary
backcompat path instead of only handling MemberQueryParams. Update the
get_members method in Workspaces so callers can still pass a plain mapping like
{"is_active": True} without hitting params.to_query_params(); keep the existing
MemberQueryParams behavior while adding a branch that forwards raw dict-like
params directly to _get. Ensure the response parsing into WorkspaceMember stays
unchanged.
---
Duplicate comments:
In `@plane/api/projects.py`:
- Around line 112-119: Empty mapping filter values are still being sent through
in the project members query path. Update the params handling in the
project-members request flow around the MemberQueryParams / raw mapping branch
so blank strings and None are filtered out before calling _get(...), while still
lowercasing bools for the typed filter backend. Keep the behavior consistent
with the existing query param conversion helper used by the project member
listing logic.
In `@plane/api/workspaces.py`:
- Around line 31-33: The member-list endpoints in the workspace API calls are
missing the required trailing slash, which can break routing on slash-sensitive
servers. Update the request path built in the workspace members helper to use
the trailing-slash form, and apply the same fix to the other related members
call referenced in this review so the endpoint path consistently follows the API
convention. Look for the self._get usages in the workspace members methods and
ensure the final URL segment ends with /.
In `@plane/models/query_params.py`:
- Around line 123-132: The to_query_params method in QueryParams still
serializes empty string filters because model_dump(exclude_none=True) only
removes None values. Update the serialization logic to also drop blank text
values like "" and whitespace-only strings before building the query-param dict,
while preserving the existing boolean lowercasing behavior for accepted fields.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 62b6d6e1-77f8-42a1-bf63-fbf6470cd916
📒 Files selected for processing (10)
README.mdplane/api/projects.pyplane/api/workspaces.pyplane/models/__init__.pyplane/models/projects.pyplane/models/query_params.pyplane/models/workspaces.pypyproject.tomltests/unit/test_projects.pytests/unit/test_workspaces.py
✅ Files skipped from review due to trivial changes (1)
- pyproject.toml
🚧 Files skipped from review as they are similar to previous changes (4)
- plane/models/init.py
- tests/unit/test_projects.py
- tests/unit/test_workspaces.py
- plane/models/workspaces.py
| response = self._get( | ||
| f"{workspace_slug}/projects/{project_id}/project-members", params=params |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
These member endpoints should include the trailing slash.
The docstring already describes /project-members/, but both requests still hit slashless paths. That violates the API endpoint convention for plane/api/**/*.py and risks route mismatches on stricter servers. As per path instructions, All API endpoints should end with a trailing / and follow URL convention: {base_path}/api/v1{resource_base_path}/{endpoint}/.
Also applies to: 141-143
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plane/api/projects.py` around lines 118 - 119, The project member endpoints
in the `Projects` API are missing the required trailing slash, which can cause
route mismatches on stricter servers. Update the `_get` calls for the
`project-members` and related member endpoint in `plane/api/projects.py` to use
the conventionally formatted path ending in `/`, matching the docstring and the
API URL pattern used throughout `plane/api/**/*.py`.
Source: Path instructions
| def get_members( | ||
| self, workspace_slug: str | ||
| self, workspace_slug: str, params: MemberQueryParams | None = None | ||
| ) -> list[WorkspaceMember]: | ||
| """Get all members of a workspace. | ||
| """Get all members of a workspace (unpaginated). | ||
|
|
||
| Returns a list of WorkspaceMember objects that include role (int) and | ||
| role_slug (str) fields in addition to basic identity fields. | ||
|
|
||
| Args: | ||
| workspace_slug: The workspace slug identifier | ||
| params: Optional filter query parameters (first_name, last_name, | ||
| email, display_name, role_slug, is_active, is_bot) | ||
| """ | ||
| response = self._get(f"{workspace_slug}/members") | ||
| response = self._get( | ||
| f"{workspace_slug}/members", | ||
| params=params.to_query_params() if params else None, | ||
| ) | ||
| return [WorkspaceMember.model_validate(item) for item in response or []] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Workspaces.get_members() lost the raw-mapping backcompat path.
The PR contract says workspace member listing still accepts raw dictionaries, but this signature only accepts MemberQueryParams and immediately calls to_query_params(). Existing callers that pass {"is_active": True} will now fail before the request is made.
Suggested fix
+from collections.abc import Mapping
+
def get_members(
- self, workspace_slug: str, params: MemberQueryParams | None = None
+ self,
+ workspace_slug: str,
+ params: MemberQueryParams | Mapping[str, Any] | None = None,
) -> list[WorkspaceMember]:
@@
- response = self._get(
- f"{workspace_slug}/members",
- params=params.to_query_params() if params else None,
- )
+ if isinstance(params, MemberQueryParams):
+ params = params.to_query_params()
+ elif params is not None:
+ params = {
+ k: (str(v).lower() if isinstance(v, bool) else v)
+ for k, v in params.items()
+ }
+ response = self._get(f"{workspace_slug}/members", params=params)
return [WorkspaceMember.model_validate(item) for item in response or []]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def get_members( | |
| self, workspace_slug: str | |
| self, workspace_slug: str, params: MemberQueryParams | None = None | |
| ) -> list[WorkspaceMember]: | |
| """Get all members of a workspace. | |
| """Get all members of a workspace (unpaginated). | |
| Returns a list of WorkspaceMember objects that include role (int) and | |
| role_slug (str) fields in addition to basic identity fields. | |
| Args: | |
| workspace_slug: The workspace slug identifier | |
| params: Optional filter query parameters (first_name, last_name, | |
| email, display_name, role_slug, is_active, is_bot) | |
| """ | |
| response = self._get(f"{workspace_slug}/members") | |
| response = self._get( | |
| f"{workspace_slug}/members", | |
| params=params.to_query_params() if params else None, | |
| ) | |
| return [WorkspaceMember.model_validate(item) for item in response or []] | |
| from collections.abc import Mapping | |
| def get_members( | |
| self, | |
| workspace_slug: str, | |
| params: MemberQueryParams | Mapping[str, Any] | None = None, | |
| ) -> list[WorkspaceMember]: | |
| """Get all members of a workspace (unpaginated). | |
| Returns a list of WorkspaceMember objects that include role (int) and | |
| role_slug (str) fields in addition to basic identity fields. | |
| Args: | |
| workspace_slug: The workspace slug identifier | |
| params: Optional filter query parameters (first_name, last_name, | |
| email, display_name, role_slug, is_active, is_bot) | |
| """ | |
| if isinstance(params, MemberQueryParams): | |
| params = params.to_query_params() | |
| elif params is not None: | |
| params = { | |
| k: (str(v).lower() if isinstance(v, bool) else v) | |
| for k, v in params.items() | |
| } | |
| response = self._get( | |
| f"{workspace_slug}/members", | |
| params=params, | |
| ) | |
| return [WorkspaceMember.model_validate(item) for item in response or []] |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@plane/api/workspaces.py` around lines 18 - 35, Workspaces.get_members()
should preserve the raw-dictionary backcompat path instead of only handling
MemberQueryParams. Update the get_members method in Workspaces so callers can
still pass a plain mapping like {"is_active": True} without hitting
params.to_query_params(); keep the existing MemberQueryParams behavior while
adding a branch that forwards raw dict-like params directly to _get. Ensure the
response parsing into WorkspaceMember stays unchanged.
Description
Added support for filtering workspace and project members through SDK member listing APIs.
This change introduces typed query parameters that mirror the filtering capabilities available in the external REST APIs.
Changes
MemberQueryParamsmodel for member filtering.client.workspaces.get_members()client.projects.get_members()first_namelast_nameemaildisplay_namerole_slugis_activeis_botMemberQueryParamsfromplane.models.Backward Compatibility
Type of Change
Test Scenarios
Summary by CodeRabbit
MemberQueryParams, includingfirst_name/last_name/email/display_name,role_slug, andis_active/is_bot.MemberListQueryParams, returning a paginated envelope withresultsandtotal_count.is_activeandis_botfields (for both workspaces and projects).