-
Notifications
You must be signed in to change notification settings - Fork 674
feat(api): add support for project feature flags and feature flag user lists #3366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bfc0637
3e2385a
9a9322d
0915bfc
02e5535
07d98be
7e287d2
522c25e
8d21b07
977ab5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| ############################### | ||
| Project Feature Flag User Lists | ||
| ############################### | ||
|
|
||
| Reference | ||
| --------- | ||
|
|
||
| * v4 API: | ||
|
|
||
| + :class:`gitlab.v4.objects.ProjectFeatureFlagUserList` | ||
| + :class:`gitlab.v4.objects.ProjectFeatureFlagUserListManager` | ||
| + :attr:`gitlab.v4.objects.Project.feature_flags_user_lists` | ||
|
|
||
| * GitLab API: https://docs.gitlab.com/ee/api/feature_flag_user_lists.html | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like it should be: https://docs.gitlab.com/api/feature_flag_user_lists/ |
||
|
|
||
| Examples | ||
| -------- | ||
|
|
||
| List user lists:: | ||
|
|
||
| user_lists = project.feature_flags_user_lists.list() | ||
|
|
||
| Get a user list:: | ||
|
|
||
| user_list = project.feature_flags_user_lists.get(list_iid) | ||
|
|
||
| Create a user list:: | ||
|
|
||
| user_list = project.feature_flags_user_lists.create({ | ||
| 'name': 'my_user_list', | ||
| 'user_xids': 'user1,user2,user3' | ||
| }) | ||
|
|
||
| Update a user list:: | ||
|
|
||
| user_list.name = 'updated_list_name' | ||
| user_list.user_xids = 'user1,user2' | ||
| user_list.save() | ||
|
|
||
| Delete a user list:: | ||
|
|
||
| user_list.delete() | ||
|
|
||
| Search for a user list:: | ||
|
|
||
| user_lists = project.feature_flags_user_lists.list(search='my_list') | ||
|
|
||
| See also | ||
| -------- | ||
|
|
||
| * :doc:`project_feature_flags` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| ##################### | ||
| Project Feature Flags | ||
| ##################### | ||
|
|
||
| Reference | ||
| --------- | ||
|
|
||
| * v4 API: | ||
|
|
||
| + :class:`gitlab.v4.objects.ProjectFeatureFlag` | ||
| + :class:`gitlab.v4.objects.ProjectFeatureFlagManager` | ||
| + :attr:`gitlab.v4.objects.Project.feature_flags` | ||
|
|
||
| * GitLab API: https://docs.gitlab.com/ee/api/feature_flags.html | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like it should be: https://docs.gitlab.com/api/feature_flags/ |
||
|
|
||
| Examples | ||
| -------- | ||
|
|
||
| List feature flags:: | ||
|
|
||
| flags = project.feature_flags.list() | ||
|
|
||
| Get a feature flag:: | ||
|
|
||
| flag = project.feature_flags.get('my_feature_flag') | ||
|
|
||
| Create a feature flag:: | ||
|
|
||
| flag = project.feature_flags.create({'name': 'my_feature_flag', 'version': 'new_version_flag'}) | ||
|
|
||
| Create a feature flag with strategies:: | ||
|
|
||
| flag = project.feature_flags.create({ | ||
| 'name': 'my_complex_flag', | ||
| 'version': 'new_version_flag', | ||
| 'strategies': [{ | ||
| 'name': 'userWithId', | ||
| 'parameters': {'userIds': 'user1,user2'} | ||
| }] | ||
| }) | ||
|
|
||
| Update a feature flag:: | ||
|
|
||
| flag.description = 'Updated description' | ||
| flag.save() | ||
|
|
||
| Rename a feature flag:: | ||
|
|
||
| # You can rename a flag by changing its name attribute and calling save() | ||
| flag.name = 'new_flag_name' | ||
| flag.save() | ||
|
|
||
| # Alternatively, you can use the manager's update method | ||
| project.feature_flags.update('old_flag_name', {'name': 'new_flag_name'}) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the CLI do this? |
||
|
|
||
| Delete a feature flag:: | ||
|
|
||
| flag.delete() | ||
|
|
||
| See also | ||
| -------- | ||
|
|
||
| * :doc:`project_feature_flag_user_lists` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -409,6 +409,44 @@ Search projects by custom attribute:: | |
| project.customattributes.set('type', 'internal') | ||
| gl.projects.list(custom_attributes={'type': 'internal'}, get_all=True) | ||
|
|
||
| Project feature flags | ||
| ===================== | ||
|
|
||
| Reference | ||
| --------- | ||
|
|
||
| * v4 API: | ||
|
|
||
| + :class:`gitlab.v4.objects.ProjectFeatureFlag` | ||
| + :class:`gitlab.v4.objects.ProjectFeatureFlagManager` | ||
| + :attr:`gitlab.v4.objects.Project.feature_flags` | ||
|
|
||
| * GitLab API: https://docs.gitlab.com/ee/api/feature_flags.html | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| Examples | ||
| -------- | ||
|
|
||
| See :doc:`project_feature_flags`. | ||
|
|
||
| Project feature flag user lists | ||
| =============================== | ||
|
|
||
| Reference | ||
| --------- | ||
|
|
||
| * v4 API: | ||
|
|
||
| + :class:`gitlab.v4.objects.ProjectFeatureFlagUserList` | ||
| + :class:`gitlab.v4.objects.ProjectFeatureFlagUserListManager` | ||
| + :attr:`gitlab.v4.objects.Project.feature_flags_user_lists` | ||
|
|
||
| * GitLab API: https://docs.gitlab.com/ee/api/feature_flag_user_lists.html | ||
|
|
||
| Examples | ||
| -------- | ||
|
|
||
| See :doc:`project_feature_flag_user_lists`. | ||
|
|
||
| Project files | ||
| ============= | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import dataclasses | ||
| import json | ||
| from typing import Any, TYPE_CHECKING | ||
|
|
||
|
|
||
|
|
@@ -49,6 +50,14 @@ def get_for_api(self, *, key: str) -> tuple[str, Any]: | |
| return (key, self._value) | ||
|
|
||
|
|
||
| class JsonAttribute(GitlabAttribute): | ||
| def set_from_cli(self, cli_value: str) -> None: | ||
| if not cli_value.strip(): | ||
| self._value = None | ||
| else: | ||
| self._value = json.loads(cli_value) | ||
|
Comment on lines
+56
to
+58
|
||
|
|
||
|
|
||
| class _ListArrayAttribute(GitlabAttribute): | ||
| """Helper class to support `list` / `array` types.""" | ||
|
|
||
|
|
@@ -87,6 +96,16 @@ class CommaSeparatedListAttribute(_ListArrayAttribute): | |
| into a CSV""" | ||
|
|
||
|
|
||
| class CommaSeparatedStringAttribute(_ListArrayAttribute): | ||
| """ | ||
| For values which are sent to the server as a Comma Separated Values (CSV) string. | ||
| Unlike CommaSeparatedListAttribute, this type ensures the value is converted | ||
| to a string even in JSON bodies (POST/PUT requests). | ||
| """ | ||
|
|
||
| transform_in_post = True | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some explanation of this would be useful as it isn't that easy to figure out. Also could |
||
|
|
||
|
|
||
| class LowercaseStringAttribute(GitlabAttribute): | ||
| def get_for_api(self, *, key: str) -> tuple[str, str]: | ||
| return (key, str(self._value).lower()) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -198,7 +198,9 @@ def _transform_types( | |
| files[attr_name] = (key, data.pop(attr_name)) | ||
| continue | ||
|
|
||
| if not transform_data: | ||
| if not transform_data and not getattr( | ||
| gitlab_attribute, "transform_in_post", False | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some explanation of this would be useful as it isn't that easy to figure out. Also could |
||
| ): | ||
| continue | ||
|
|
||
| if isinstance(gitlab_attribute, types.GitlabAttribute): | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| """ | ||
| GitLab API: | ||
| https://docs.gitlab.com/ee/api/feature_flag_user_lists.html | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from gitlab import types | ||
| from gitlab.base import RESTObject | ||
| from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin | ||
| from gitlab.types import RequiredOptional | ||
|
|
||
| __all__ = ["ProjectFeatureFlagUserList", "ProjectFeatureFlagUserListManager"] | ||
|
|
||
|
|
||
| class ProjectFeatureFlagUserList(SaveMixin, ObjectDeleteMixin, RESTObject): | ||
| _id_attr = "iid" | ||
|
|
||
|
|
||
| class ProjectFeatureFlagUserListManager(CRUDMixin[ProjectFeatureFlagUserList]): | ||
| _path = "/projects/{project_id}/feature_flags_user_lists" | ||
| _obj_cls = ProjectFeatureFlagUserList | ||
| _from_parent_attrs = {"project_id": "id"} | ||
| _create_attrs = RequiredOptional(required=("name", "user_xids")) | ||
| _update_attrs = RequiredOptional(optional=("name", "user_xids")) | ||
| _list_filters = ("search",) | ||
| _types = {"user_xids": types.CommaSeparatedStringAttribute} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| """ | ||
| GitLab API: | ||
| https://docs.gitlab.com/ee/api/feature_flags.html | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any | ||
|
|
||
| from gitlab import types, utils | ||
| from gitlab.base import RESTObject | ||
| from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin | ||
| from gitlab.types import RequiredOptional | ||
|
|
||
| __all__ = ["ProjectFeatureFlag", "ProjectFeatureFlagManager"] | ||
|
|
||
|
|
||
| class ProjectFeatureFlag(SaveMixin, ObjectDeleteMixin, RESTObject): | ||
| _id_attr = "name" | ||
| manager: ProjectFeatureFlagManager | ||
|
|
||
| def _get_save_url_id(self) -> str | int | None: | ||
| """Get the ID used to construct the API URL for the save operation. | ||
|
|
||
| For renames, this must be the *original* name of the flag. For other | ||
| updates, it is the current name. | ||
| """ | ||
| if self._id_attr in self._updated_attrs: | ||
| # If the name is being changed, use the original name for the URL. | ||
| obj_id = self._attrs.get(self._id_attr) | ||
| if isinstance(obj_id, str): | ||
| return utils.EncodedId(obj_id) | ||
| return obj_id | ||
| return self.encoded_id | ||
|
|
||
| def save(self, **kwargs: Any) -> dict[str, Any] | None: | ||
| """Save the changes made to the object to the server. | ||
|
|
||
| The object is updated to match what the server returns. | ||
|
|
||
| This method overrides the default ``save()`` method to handle renaming | ||
| feature flags. When the name is modified, the API requires the original | ||
| name in the URL to identify the resource, while the new name is sent | ||
| in the request body. | ||
|
|
||
| Args: | ||
| **kwargs: Extra options to send to the server (e.g. sudo) | ||
|
|
||
| Returns: | ||
| The new object data (*not* a RESTObject) | ||
|
|
||
| Raises: | ||
| GitlabAuthenticationError: If authentication is not correct | ||
| GitlabUpdateError: If the server cannot perform the request | ||
| """ | ||
| updated_data = self._get_updated_data() | ||
| if not updated_data: | ||
| return None | ||
|
|
||
| obj_id = self._get_save_url_id() | ||
| server_data = self.manager.update(obj_id, updated_data, **kwargs) | ||
| self._update_attrs(server_data) | ||
| return server_data | ||
|
|
||
|
|
||
| class ProjectFeatureFlagManager(CRUDMixin[ProjectFeatureFlag]): | ||
| _path = "/projects/{project_id}/feature_flags" | ||
| _obj_cls = ProjectFeatureFlag | ||
| _from_parent_attrs = {"project_id": "id"} | ||
| _create_attrs = RequiredOptional( | ||
| required=("name",), optional=("version", "description", "active", "strategies") | ||
| ) | ||
| _update_attrs = RequiredOptional( | ||
| optional=("name", "description", "active", "strategies") | ||
| ) | ||
| _list_filters = ("scope",) | ||
| _types = {"strategies": types.JsonAttribute} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this changed?
The previous version seems correct.