Skip to content

Commit cf07548

Browse files
committed
feat(api): add support for project feature flag user lists
This adds support for managing user lists for project feature flags. It introduces the `ProjectFeatureFlagUserList` object and manager, and exposes it via `project.feature_flags_user_lists`. New type `CommaSeparatedStringAttribute` is added to handle comma-separated string values in API requests.
1 parent c36cc8a commit cf07548

File tree

7 files changed

+219
-1
lines changed

7 files changed

+219
-1
lines changed

gitlab/types.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ class CommaSeparatedListAttribute(_ListArrayAttribute):
9696
into a CSV"""
9797

9898

99+
class CommaSeparatedStringAttribute(_ListArrayAttribute):
100+
"""
101+
For values which are sent to the server as a Comma Separated Values (CSV) string.
102+
Unlike CommaSeparatedListAttribute, this type ensures the value is converted
103+
to a string even in JSON bodies (POST/PUT requests).
104+
"""
105+
106+
transform_in_post = True
107+
108+
99109
class LowercaseStringAttribute(GitlabAttribute):
100110
def get_for_api(self, *, key: str) -> tuple[str, str]:
101111
return (key, str(self._value).lower())

gitlab/utils.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,9 @@ def _transform_types(
198198
files[attr_name] = (key, data.pop(attr_name))
199199
continue
200200

201-
if not transform_data:
201+
if not transform_data and not getattr(
202+
gitlab_attribute, "transform_in_post", False
203+
):
202204
continue
203205

204206
if isinstance(gitlab_attribute, types.GitlabAttribute):

gitlab/v4/objects/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .epics import *
2525
from .events import *
2626
from .export_import import *
27+
from .feature_flag_user_lists import *
2728
from .feature_flags import *
2829
from .features import *
2930
from .files import *
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
GitLab API:
3+
https://docs.gitlab.com/ee/api/feature_flag_user_lists.html
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from gitlab import types
9+
from gitlab.base import RESTObject
10+
from gitlab.mixins import CRUDMixin, ObjectDeleteMixin, SaveMixin
11+
from gitlab.types import RequiredOptional
12+
13+
__all__ = ["ProjectFeatureFlagUserList", "ProjectFeatureFlagUserListManager"]
14+
15+
16+
class ProjectFeatureFlagUserList(SaveMixin, ObjectDeleteMixin, RESTObject):
17+
_id_attr = "iid"
18+
19+
20+
class ProjectFeatureFlagUserListManager(CRUDMixin[ProjectFeatureFlagUserList]):
21+
_path = "/projects/{project_id}/feature_flags_user_lists"
22+
_obj_cls = ProjectFeatureFlagUserList
23+
_from_parent_attrs = {"project_id": "id"}
24+
_create_attrs = RequiredOptional(required=("name", "user_xids"))
25+
_update_attrs = RequiredOptional(optional=("name", "user_xids"))
26+
_list_filters = ("search",)
27+
_types = {"user_xids": types.CommaSeparatedStringAttribute}

gitlab/v4/objects/projects.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
)
5050
from .events import ProjectEventManager # noqa: F401
5151
from .export_import import ProjectExportManager, ProjectImportManager # noqa: F401
52+
from .feature_flag_user_lists import ProjectFeatureFlagUserListManager # noqa: F401
5253
from .feature_flags import ProjectFeatureFlagManager # noqa: F401
5354
from .files import ProjectFileManager # noqa: F401
5455
from .hooks import ProjectHookManager # noqa: F401
@@ -203,6 +204,7 @@ class Project(
203204
events: ProjectEventManager
204205
exports: ProjectExportManager
205206
feature_flags: ProjectFeatureFlagManager
207+
feature_flags_user_lists: ProjectFeatureFlagUserListManager
206208
files: ProjectFileManager
207209
forks: ProjectForkManager
208210
generic_packages: GenericPackageManager
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import pytest
2+
3+
from gitlab import exceptions
4+
5+
6+
@pytest.fixture
7+
def user_list(project, user):
8+
user_list = project.feature_flags_user_lists.create(
9+
{"name": "test_user_list", "user_xids": str(user.id)}
10+
)
11+
yield user_list
12+
try:
13+
user_list.delete()
14+
except exceptions.GitlabDeleteError:
15+
pass
16+
17+
18+
def test_create_user_list(project, user):
19+
user_list = project.feature_flags_user_lists.create(
20+
{"name": "created_user_list", "user_xids": str(user.id)}
21+
)
22+
assert user_list.name == "created_user_list"
23+
assert str(user.id) in user_list.user_xids
24+
user_list.delete()
25+
26+
27+
def test_list_user_lists(project, user_list):
28+
ff_user_lists = project.feature_flags_user_lists.list()
29+
assert len(ff_user_lists) >= 1
30+
assert user_list.iid in [ff_user.iid for ff_user in ff_user_lists]
31+
32+
33+
def test_get_user_list(project, user_list, user):
34+
retrieved_list = project.feature_flags_user_lists.get(user_list.iid)
35+
assert retrieved_list.name == user_list.name
36+
assert str(user.id) in retrieved_list.user_xids
37+
38+
39+
def test_update_user_list(project, user_list):
40+
user_list.name = "updated_user_list"
41+
user_list.save()
42+
43+
updated_list = project.feature_flags_user_lists.get(user_list.iid)
44+
assert updated_list.name == "updated_user_list"
45+
46+
47+
def test_delete_user_list(project, user_list):
48+
user_list.delete()
49+
with pytest.raises(exceptions.GitlabGetError):
50+
project.feature_flags_user_lists.get(user_list.iid)
51+
52+
53+
def test_search_user_list(project, user_list):
54+
ff_user_lists = project.feature_flags_user_lists.list(search=user_list.name)
55+
assert len(ff_user_lists) >= 1
56+
assert user_list.iid in [ff_user.iid for ff_user in ff_user_lists]
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import json
2+
3+
import pytest
4+
5+
6+
@pytest.fixture
7+
def user_list_cli(gitlab_cli, project, user):
8+
list_name = "cli_test_list_fixture"
9+
cmd = [
10+
"-o",
11+
"json",
12+
"project-feature-flag-user-list",
13+
"create",
14+
"--project-id",
15+
str(project.id),
16+
"--name",
17+
list_name,
18+
"--user-xids",
19+
str(user.id),
20+
]
21+
ret = gitlab_cli(cmd)
22+
data = json.loads(ret.stdout)
23+
iid = str(data["iid"])
24+
25+
yield iid
26+
27+
try:
28+
cmd = [
29+
"project-feature-flag-user-list",
30+
"delete",
31+
"--project-id",
32+
str(project.id),
33+
"--iid",
34+
iid,
35+
]
36+
gitlab_cli(cmd)
37+
except Exception:
38+
pass
39+
40+
41+
def test_project_feature_flag_user_list_cli_create_delete(gitlab_cli, project, user):
42+
list_name = "cli_test_list_create"
43+
44+
cmd = [
45+
"-o",
46+
"json",
47+
"project-feature-flag-user-list",
48+
"create",
49+
"--project-id",
50+
str(project.id),
51+
"--name",
52+
list_name,
53+
"--user-xids",
54+
str(user.id),
55+
]
56+
ret = gitlab_cli(cmd)
57+
assert ret.success
58+
data = json.loads(ret.stdout)
59+
assert data["name"] == list_name
60+
assert str(user.id) in data["user_xids"]
61+
iid = str(data["iid"])
62+
63+
cmd = [
64+
"project-feature-flag-user-list",
65+
"delete",
66+
"--project-id",
67+
str(project.id),
68+
"--iid",
69+
iid,
70+
]
71+
ret = gitlab_cli(cmd)
72+
assert ret.success
73+
74+
75+
def test_project_feature_flag_user_list_cli_list(gitlab_cli, project, user_list_cli):
76+
cmd = [
77+
"-o",
78+
"json",
79+
"project-feature-flag-user-list",
80+
"list",
81+
"--project-id",
82+
str(project.id),
83+
]
84+
ret = gitlab_cli(cmd)
85+
assert ret.success
86+
data = json.loads(ret.stdout)
87+
assert any(item["name"] == "cli_test_list_fixture" for item in data)
88+
89+
90+
def test_project_feature_flag_user_list_cli_get(gitlab_cli, project, user_list_cli):
91+
cmd = [
92+
"-o",
93+
"json",
94+
"project-feature-flag-user-list",
95+
"get",
96+
"--project-id",
97+
str(project.id),
98+
"--iid",
99+
user_list_cli,
100+
]
101+
ret = gitlab_cli(cmd)
102+
assert ret.success
103+
data = json.loads(ret.stdout)
104+
assert data["name"] == "cli_test_list_fixture"
105+
106+
107+
def test_project_feature_flag_user_list_cli_update(gitlab_cli, project, user_list_cli):
108+
new_name = "cli_updated_list"
109+
cmd = [
110+
"project-feature-flag-user-list",
111+
"update",
112+
"--project-id",
113+
str(project.id),
114+
"--iid",
115+
user_list_cli,
116+
"--name",
117+
new_name,
118+
]
119+
ret = gitlab_cli(cmd)
120+
assert ret.success

0 commit comments

Comments
 (0)