Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit7afd340

Browse files
nejchJohnVillalovos
authored andcommitted
feat: add support for group and project invitations API
1 parent4794ecc commit7afd340

File tree

8 files changed

+331
-0
lines changed

8 files changed

+331
-0
lines changed

‎docs/api-objects.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ API examples
2626
gl_objects/geo_nodes
2727
gl_objects/groups
2828
gl_objects/group_access_tokens
29+
gl_objects/invitations
2930
gl_objects/issues
3031
gl_objects/keys
3132
gl_objects/boards

‎docs/gl_objects/invitations.rst‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
###########
2+
Invitations
3+
###########
4+
5+
Invitations let you invite or add users to a group or project.
6+
7+
Reference
8+
---------
9+
10+
* v4 API:
11+
12+
+:class:`gitlab.v4.objects.GroupInvitation`
13+
+:class:`gitlab.v4.objects.GroupInvitationManager`
14+
+:attr:`gitlab.v4.objects.Group.invitations`
15+
+:class:`gitlab.v4.objects.ProjectInvitation`
16+
+:class:`gitlab.v4.objects.ProjectInvitationManager`
17+
+:attr:`gitlab.v4.objects.Project.invitations`
18+
19+
* GitLab API: https://docs.gitlab.com/ce/api/invitations.html
20+
21+
Examples
22+
--------
23+
24+
..danger::
25+
26+
Creating an invitation with ``create()`` returns a status response,
27+
rather than invitation details, because it allows sending multiple
28+
invitations at the same time.
29+
30+
Thus when using several emails, you do not create a real invitation
31+
object you can manipulate, because python-gitlab cannot know which email
32+
to track as the ID.
33+
34+
In that case, use a **lazy** ``get()`` method shown below using a specific
35+
email address to create an invitation object you can manipulate.
36+
37+
Create an invitation::
38+
39+
invitation = group_or_project.invitations.create(
40+
{
41+
"email": "email@example.com",
42+
"access_level": gitlab.const.AccessLevel.DEVELOPER,
43+
}
44+
)
45+
46+
List invitations for a group or project::
47+
48+
invitations = group_or_project.invitations.list()
49+
50+
..warning::
51+
52+
As mentioned above, GitLab does not provide a real GET endpoint for a single
53+
invitation. We can create a lazy object to later manipulate it.
54+
55+
Update an invitation::
56+
57+
invitation = group_or_project.invitations.get("email@example.com", lazy=True)
58+
invitation.access_level = gitlab.const.AccessLevel.DEVELOPER
59+
invitation.save()
60+
61+
# or
62+
group_or_project.invitations.update(
63+
"email@example.com",
64+
{"access_level": gitlab.const.AccessLevel.DEVELOPER}
65+
)
66+
67+
Delete an invitation::
68+
69+
invitation = group_or_project.invitations.get("email@example.com", lazy=True)
70+
invitation.delete()
71+
72+
# or
73+
group_or_project.invitations.delete("email@example.com")

‎gitlab/exceptions.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,10 @@ class GitlabImportError(GitlabOperationError):
250250
pass
251251

252252

253+
classGitlabInvitationError(GitlabOperationError):
254+
pass
255+
256+
253257
classGitlabCherryPickError(GitlabOperationError):
254258
pass
255259

‎gitlab/v4/objects/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from .groupsimport*
4646
from .hooksimport*
4747
from .integrationsimport*
48+
from .invitationsimport*
4849
from .issuesimport*
4950
from .jobsimport*
5051
from .keysimport*

‎gitlab/v4/objects/groups.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from .export_importimportGroupExportManager,GroupImportManager# noqa: F401
2323
from .group_access_tokensimportGroupAccessTokenManager# noqa: F401
2424
from .hooksimportGroupHookManager# noqa: F401
25+
from .invitationsimportGroupInvitationManager# noqa: F401
2526
from .issuesimportGroupIssueManager# noqa: F401
2627
from .labelsimportGroupLabelManager# noqa: F401
2728
from .membersimport (# noqa: F401
@@ -67,6 +68,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject):
6768
exports:GroupExportManager
6869
hooks:GroupHookManager
6970
imports:GroupImportManager
71+
invitations:GroupInvitationManager
7072
issues:GroupIssueManager
7173
issues_statistics:GroupIssuesStatisticsManager
7274
labels:GroupLabelManager

‎gitlab/v4/objects/invitations.py‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
fromtypingimportAny,cast,Union
2+
3+
fromgitlab.baseimportRESTManager,RESTObject
4+
fromgitlab.exceptionsimportGitlabInvitationError
5+
fromgitlab.mixinsimportCRUDMixin,ObjectDeleteMixin,SaveMixin
6+
fromgitlab.typesimportCommaSeparatedListAttribute,RequiredOptional
7+
8+
__all__= [
9+
"ProjectInvitation",
10+
"ProjectInvitationManager",
11+
"GroupInvitation",
12+
"GroupInvitationManager",
13+
]
14+
15+
16+
classInvitationMixin(CRUDMixin):
17+
defcreate(self,*args:Any,**kwargs:Any)->RESTObject:
18+
invitation=super().create(*args,**kwargs)
19+
20+
ifinvitation.status=="error":
21+
raiseGitlabInvitationError(invitation.message)
22+
23+
returninvitation
24+
25+
26+
classProjectInvitation(SaveMixin,ObjectDeleteMixin,RESTObject):
27+
_id_attr="email"
28+
29+
30+
classProjectInvitationManager(InvitationMixin,RESTManager):
31+
_path="/projects/{project_id}/invitations"
32+
_obj_cls=ProjectInvitation
33+
_from_parent_attrs= {"project_id":"id"}
34+
_create_attrs=RequiredOptional(
35+
required=("access_level",),
36+
optional=(
37+
"expires_at",
38+
"invite_source",
39+
"tasks_to_be_done",
40+
"tasks_project_id",
41+
),
42+
exclusive=("email","user_id"),
43+
)
44+
_update_attrs=RequiredOptional(
45+
optional=("access_level","expires_at"),
46+
)
47+
_list_filters= ("query",)
48+
_types= {
49+
"email":CommaSeparatedListAttribute,
50+
"user_id":CommaSeparatedListAttribute,
51+
}
52+
53+
defget(
54+
self,id:Union[str,int],lazy:bool=False,**kwargs:Any
55+
)->ProjectInvitation:
56+
returncast(ProjectInvitation,super().get(id=id,lazy=lazy,**kwargs))
57+
58+
59+
classGroupInvitation(SaveMixin,ObjectDeleteMixin,RESTObject):
60+
_id_attr="email"
61+
62+
63+
classGroupInvitationManager(InvitationMixin,RESTManager):
64+
_path="/groups/{group_id}/invitations"
65+
_obj_cls=GroupInvitation
66+
_from_parent_attrs= {"group_id":"id"}
67+
_create_attrs=RequiredOptional(
68+
required=("access_level",),
69+
optional=(
70+
"expires_at",
71+
"invite_source",
72+
"tasks_to_be_done",
73+
"tasks_project_id",
74+
),
75+
exclusive=("email","user_id"),
76+
)
77+
_update_attrs=RequiredOptional(
78+
optional=("access_level","expires_at"),
79+
)
80+
_list_filters= ("query",)
81+
_types= {
82+
"email":CommaSeparatedListAttribute,
83+
"user_id":CommaSeparatedListAttribute,
84+
}
85+
86+
defget(
87+
self,id:Union[str,int],lazy:bool=False,**kwargs:Any
88+
)->GroupInvitation:
89+
returncast(GroupInvitation,super().get(id=id,lazy=lazy,**kwargs))

‎gitlab/v4/objects/projects.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from .filesimportProjectFileManager# noqa: F401
5656
from .hooksimportProjectHookManager# noqa: F401
5757
from .integrationsimportProjectIntegrationManager,ProjectServiceManager# noqa: F401
58+
from .invitationsimportProjectInvitationManager# noqa: F401
5859
from .issuesimportProjectIssueManager# noqa: F401
5960
from .jobsimportProjectJobManager# noqa: F401
6061
from .labelsimportProjectLabelManager# noqa: F401
@@ -179,6 +180,7 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO
179180
hooks:ProjectHookManager
180181
imports:ProjectImportManager
181182
integrations:ProjectIntegrationManager
183+
invitations:ProjectInvitationManager
182184
issues:ProjectIssueManager
183185
issues_statistics:ProjectIssuesStatisticsManager
184186
jobs:ProjectJobManager
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""
2+
GitLab API: https://docs.gitlab.com/ce/api/invitations.html
3+
"""
4+
5+
importre
6+
7+
importpytest
8+
importresponses
9+
10+
fromgitlab.exceptionsimportGitlabInvitationError
11+
12+
create_content= {"email":"email@example.com","access_level":30}
13+
success_content= {"status":"success"}
14+
error_content= {
15+
"status":"error",
16+
"message": {
17+
"test@example.com":"Invite email has already been taken",
18+
"test2@example.com":"User already exists in source",
19+
"test_username":"Access level is not included in the list",
20+
},
21+
}
22+
invitations_content= [
23+
{
24+
"id":1,
25+
"invite_email":"member@example.org",
26+
"created_at":"2020-10-22T14:13:35Z",
27+
"access_level":30,
28+
"expires_at":"2020-11-22T14:13:35Z",
29+
"user_name":"Raymond Smith",
30+
"created_by_name":"Administrator",
31+
},
32+
]
33+
invitation_content= {
34+
"expires_at":"2012-10-22T14:13:35Z",
35+
"access_level":40,
36+
}
37+
38+
39+
@pytest.fixture
40+
defresp_invitations_list():
41+
withresponses.RequestsMock()asrsps:
42+
rsps.add(
43+
method=responses.GET,
44+
url=re.compile(r"http://localhost/api/v4/(groups|projects)/1/invitations"),
45+
json=invitations_content,
46+
content_type="application/json",
47+
status=200,
48+
)
49+
yieldrsps
50+
51+
52+
@pytest.fixture
53+
defresp_invitation_create():
54+
withresponses.RequestsMock()asrsps:
55+
rsps.add(
56+
method=responses.POST,
57+
url=re.compile(r"http://localhost/api/v4/(groups|projects)/1/invitations"),
58+
json=success_content,
59+
content_type="application/json",
60+
status=200,
61+
)
62+
yieldrsps
63+
64+
65+
@pytest.fixture
66+
defresp_invitation_create_error():
67+
withresponses.RequestsMock()asrsps:
68+
rsps.add(
69+
method=responses.POST,
70+
url=re.compile(r"http://localhost/api/v4/(groups|projects)/1/invitations"),
71+
json=error_content,
72+
content_type="application/json",
73+
status=200,
74+
)
75+
yieldrsps
76+
77+
78+
@pytest.fixture
79+
defresp_invitation_update():
80+
withresponses.RequestsMock()asrsps:
81+
pattern=re.compile(
82+
r"http://localhost/api/v4/(groups|projects)/1/invitations/email%40example.com"
83+
)
84+
rsps.add(
85+
method=responses.PUT,
86+
url=pattern,
87+
json=invitation_content,
88+
content_type="application/json",
89+
status=200,
90+
)
91+
yieldrsps
92+
93+
94+
@pytest.fixture
95+
defresp_invitation_delete():
96+
withresponses.RequestsMock()asrsps:
97+
pattern=re.compile(
98+
r"http://localhost/api/v4/(groups|projects)/1/invitations/email%40example.com"
99+
)
100+
rsps.add(
101+
method=responses.DELETE,
102+
url=pattern,
103+
status=204,
104+
)
105+
yieldrsps
106+
107+
108+
deftest_list_group_invitations(group,resp_invitations_list):
109+
invitations=group.invitations.list()
110+
assertinvitations[0].invite_email=="member@example.org"
111+
112+
113+
deftest_create_group_invitation(group,resp_invitation_create):
114+
invitation=group.invitations.create(create_content)
115+
assertinvitation.status=="success"
116+
117+
118+
deftest_update_group_invitation(group,resp_invitation_update):
119+
invitation=group.invitations.get("email@example.com",lazy=True)
120+
invitation.access_level=30
121+
invitation.save()
122+
123+
124+
deftest_delete_group_invitation(group,resp_invitation_delete):
125+
invitation=group.invitations.get("email@example.com",lazy=True)
126+
invitation.delete()
127+
group.invitations.delete("email@example.com")
128+
129+
130+
deftest_list_project_invitations(project,resp_invitations_list):
131+
invitations=project.invitations.list()
132+
assertinvitations[0].invite_email=="member@example.org"
133+
134+
135+
deftest_create_project_invitation(project,resp_invitation_create):
136+
invitation=project.invitations.create(create_content)
137+
assertinvitation.status=="success"
138+
139+
140+
deftest_update_project_invitation(project,resp_invitation_update):
141+
invitation=project.invitations.get("email@example.com",lazy=True)
142+
invitation.access_level=30
143+
invitation.save()
144+
145+
146+
deftest_delete_project_invitation(project,resp_invitation_delete):
147+
invitation=project.invitations.get("email@example.com",lazy=True)
148+
invitation.delete()
149+
project.invitations.delete("email@example.com")
150+
151+
152+
deftest_create_group_invitation_raises(group,resp_invitation_create_error):
153+
withpytest.raises(GitlabInvitationError,match="User already exists"):
154+
group.invitations.create(create_content)
155+
156+
157+
deftest_create_project_invitation_raises(project,resp_invitation_create_error):
158+
withpytest.raises(GitlabInvitationError,match="User already exists"):
159+
project.invitations.create(create_content)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp