Skip to content
Open
4 changes: 3 additions & 1 deletion docs/api-objects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ API examples
gl_objects/environments
gl_objects/events
gl_objects/epics
gl_objects/features
gl_objects/gitlab_features
gl_objects/geo_nodes
gl_objects/groups
gl_objects/group_access_tokens
Expand All @@ -49,6 +49,8 @@ API examples
gl_objects/pipelines_and_jobs
gl_objects/projects
gl_objects/project_access_tokens
gl_objects/project_feature_flags
gl_objects/project_feature_flag_user_lists
gl_objects/protected_branches
gl_objects/protected_container_repositories
gl_objects/protected_environments
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
##############
Features flags
##############
################################
GitLab Development Feature Flags
################################

.. note::

This API is for managing GitLab's internal development feature flags and requires administrator access.
For project-level feature flags, see :doc:`project_feature_flags`.

Reference
---------
Expand All @@ -11,7 +16,7 @@ Reference
+ :class:`gitlab.v4.objects.FeatureManager`
+ :attr:`gitlab.Gitlab.features`

* GitLab API: https://docs.gitlab.com/api/features
* GitLab API: https://docs.gitlab.com/ee/api/features.html
Copy link
Member

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.


Examples
--------
Expand All @@ -29,4 +34,4 @@ Create or set a feature::

Delete a feature::

feature.delete()
feature.delete()
51 changes: 51 additions & 0 deletions docs/gl_objects/project_feature_flag_user_lists.rst
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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`
63 changes: 63 additions & 0 deletions docs/gl_objects/project_feature_flags.rst
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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'})
Copy link
Member

Choose a reason for hiding this comment

The 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`
38 changes: 38 additions & 0 deletions docs/gl_objects/projects.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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
=============

Expand Down
19 changes: 19 additions & 0 deletions gitlab/types.py
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


Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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 transform_post just be set to be False by default in GitLabAttribute?



class LowercaseStringAttribute(GitlabAttribute):
def get_for_api(self, *, key: str) -> tuple[str, str]:
return (key, str(self._value).lower())
Expand Down
4 changes: 3 additions & 1 deletion gitlab/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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 transform_post just be set to be False by default in GitLabAttribute?

):
continue

if isinstance(gitlab_attribute, types.GitlabAttribute):
Expand Down
2 changes: 2 additions & 0 deletions gitlab/v4/objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
from .epics import *
from .events import *
from .export_import import *
from .feature_flag_user_lists import *
from .feature_flags import *
from .features import *
from .files import *
from .geo_nodes import *
Expand Down
27 changes: 27 additions & 0 deletions gitlab/v4/objects/feature_flag_user_lists.py
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
Copy link
Member

Choose a reason for hiding this comment

The 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}
77 changes: 77 additions & 0 deletions gitlab/v4/objects/feature_flags.py
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
Copy link
Member

Choose a reason for hiding this comment

The 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

Check warning on line 33 in gitlab/v4/objects/feature_flags.py

View check run for this annotation

Codecov / codecov/patch

gitlab/v4/objects/feature_flags.py#L33

Added line #L33 was not covered by tests
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

Check warning on line 58 in gitlab/v4/objects/feature_flags.py

View check run for this annotation

Codecov / codecov/patch

gitlab/v4/objects/feature_flags.py#L58

Added line #L58 was not covered by tests

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}
4 changes: 4 additions & 0 deletions gitlab/v4/objects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
)
from .events import ProjectEventManager # noqa: F401
from .export_import import ProjectExportManager, ProjectImportManager # noqa: F401
from .feature_flag_user_lists import ProjectFeatureFlagUserListManager # noqa: F401
from .feature_flags import ProjectFeatureFlagManager # noqa: F401
from .files import ProjectFileManager # noqa: F401
from .hooks import ProjectHookManager # noqa: F401
from .integrations import ProjectIntegrationManager, ProjectServiceManager # noqa: F401
Expand Down Expand Up @@ -201,6 +203,8 @@ class Project(
environments: ProjectEnvironmentManager
events: ProjectEventManager
exports: ProjectExportManager
feature_flags: ProjectFeatureFlagManager
feature_flags_user_lists: ProjectFeatureFlagUserListManager
files: ProjectFileManager
forks: ProjectForkManager
generic_packages: GenericPackageManager
Expand Down
Loading
Loading