@@ -20,7 +20,8 @@ class _Team(models.GitHubCore):
2020
2121 class_name = '_Team'
2222 # Roles available to members on a team.
23- members_roles = frozenset (['member' , 'maintainer' , 'all' ])
23+ member_roles = frozenset (['member' , 'maintainer' ])
24+ filterable_member_roles = member_roles .union (['all' ])
2425
2526 def _update_attributes (self , team ):
2627 self ._api = team ['url' ]
@@ -38,6 +39,10 @@ def _repr(self):
3839 def add_member (self , username ):
3940 """Add ``username`` to this team.
4041
42+ .. deprecated:: 1.0.0
43+
44+ Use :meth:`add_or_update_membership` instead.
45+
4146 :param str username:
4247 the username of the user you would like to add to this team.
4348 :returns:
@@ -53,6 +58,37 @@ def add_member(self, username):
5358 url = self ._build_url ('members' , username , base_url = self ._api )
5459 return self ._boolean (self ._put (url ), 204 , 404 )
5560
61+ @requires_auth
62+ def add_or_update_membership (self , username , role = 'member' ):
63+ """Add or update the user's membership in this team.
64+
65+ This returns a dictionary like so::
66+
67+ {
68+ 'state': 'pending',
69+ 'url': 'https://api.github.com/teams/...',
70+ 'role': 'member',
71+ }
72+
73+ :param str username:
74+ (required), login of user whose membership is being modified
75+ :param str role:
76+ (optional), the role the user should have once their membership
77+ has been modified. Options: 'member', 'maintainer'. Default:
78+ 'member'
79+ :returns:
80+ dictionary of the invitation response
81+ :rtype:
82+ dict
83+ """
84+ if role not in self .member_roles :
85+ raise ValueError ("'role' must be one of {}" .format (', ' .join (
86+ sorted (self .member_roles )
87+ )))
88+ data = {'role' : role }
89+ url = self ._build_url ('memberships' , username , base_url = self ._api )
90+ return self ._json (self ._put (url , json = data ), 200 )
91+
5692 @requires_auth
5793 def add_repository (self , repository , permission = '' ):
5894 """Add ``repository`` to this team.
@@ -125,6 +161,10 @@ def has_repository(self, repository):
125161 def invite (self , username ):
126162 """Invite the user to join this team.
127163
164+ .. deprecated:: 1.2.0
165+
166+ Use :meth:`add_or_update_membership` instead.
167+
128168 This returns a dictionary like so::
129169
130170 {'state': 'pending', 'url': 'https://api.github.com/teams/...'}
@@ -136,8 +176,11 @@ def invite(self, username):
136176 :rtype:
137177 dict
138178 """
139- url = self ._build_url ('memberships' , username , base_url = self ._api )
140- return self ._json (self ._put (url ), 200 )
179+ warnings .warn (
180+ 'This method is deprecated. Please use '
181+ '``add_or_update_membership`` instead.' ,
182+ DeprecationWarning )
183+ return self .add_or_update_membership (username )
141184
142185 @requires_auth
143186 def is_member (self , username ):
@@ -150,6 +193,10 @@ def is_member(self, username):
150193 :rtype:
151194 bool
152195 """
196+ warnings .warn (
197+ 'This method is deprecated. Please use '
198+ '``membership_for`` instead.' ,
199+ DeprecationWarning )
153200 url = self ._build_url ('members' , username , base_url = self ._api )
154201 return self ._boolean (self ._get (url ), 204 , 404 )
155202
@@ -173,7 +220,7 @@ def members(self, role=None, number=-1, etag=None):
173220 """
174221 headers = {}
175222 params = {}
176- if role in self .members_roles :
223+ if role in self .filterable_member_roles :
177224 params ['role' ] = role
178225 headers ['Accept' ] = 'application/vnd.github.ironman-preview+json'
179226 url = self ._build_url ('members' , base_url = self ._api )
@@ -218,6 +265,10 @@ def membership_for(self, username):
218265 def remove_member (self , username ):
219266 """Remove ``username`` from this team.
220267
268+ .. deprecated:: 1.0.0
269+
270+ Use :meth:`revoke_membership` instead.
271+
221272 :param str username:
222273 (required), username of the member to remove
223274 :returns:
@@ -374,7 +425,12 @@ class _Organization(models.GitHubCore):
374425 members_filters = frozenset (['2fa_disabled' , 'all' ])
375426
376427 # Roles available to members in an organization.
377- members_roles = frozenset (['all' , 'admin' , 'member' ])
428+ member_roles = frozenset (['admin' , 'member' ])
429+ filterable_member_roles = member_roles .union (['all' ])
430+
431+ # Roles for invitations, see also:
432+ # https://developer.github.com/v3/orgs/members/#create-organization-invitation
433+ invitation_roles = frozenset (['admin' , 'direct_member' , 'billing_manager' ])
378434
379435 def _update_attributes (self , org ):
380436 self .avatar_url = org ['avatar_url' ]
@@ -446,6 +502,31 @@ def add_member(self, username, team_id):
446502 url = self ._build_url ('teams' , str (team_id ), 'members' , str (username ))
447503 return self ._boolean (self ._put (url ), 204 , 404 )
448504
505+ @requires_auth
506+ def add_or_update_membership (self , username , role = 'member' ):
507+ """Add a member or update their role.
508+
509+ :param str username:
510+ (required), user to add or update.
511+ :param str role:
512+ (optional), role to give to the user. Options are ``member``,
513+ ``admin``. Defaults to ``member``.
514+ :returns:
515+ the created or updated membership
516+ :rtype:
517+ :class:`~github3.orgs.Membership`
518+ :raises:
519+ ValueError if role is not a valid choice
520+ """
521+ if role not in self .member_roles :
522+ raise ValueError ("'role' must be one of {}" .format (', ' .join (
523+ sorted (self .member_roles )
524+ )))
525+ data = {'role' : role }
526+ url = self ._build_url ('memberships' , str (username ), base_url = self ._api )
527+ json = self ._json (self ._put (url , json = data ), 200 )
528+ return self ._instance_or_null (Membership , json )
529+
449530 @requires_auth
450531 def add_repository (self , repository , team_id ): # FIXME(jlk): add perms
451532 """Add ``repository`` to ``team``.
@@ -627,27 +708,48 @@ def edit(self, billing_email=None, company=None, email=None, location=None,
627708 return False
628709
629710 @requires_auth
630- def invite (self , username , role = None ):
711+ def invite (self , team_ids , invitee_id = None , email = None ,
712+ role = 'direct_member' ):
631713 """Invite the user to join this organization.
632714
633- :param str username:
634- (required), user to invite to join this organization.
715+ :param list[int] team_ids:
716+ (required), list of team identifiers to invite the user to
717+ :param int invitee_id:
718+ (required if email is not specified), the identifier for the user
719+ being invited
720+ :param str email:
721+ (required if invitee_id is not specified), the email address of
722+ the user being invited
635723 :param str role:
636- (optional) role from members_roles
724+ (optional) role to provide to the invited user. Must be one of
637725 :returns:
638- dictionary resembling
639-
640- .. code-block:: python
641-
642- {'state': 'pending', 'url': 'https://api.github.com/orgs/...'}
726+ the created invitation
643727 :rtype:
644- dict
728+ :class:`~github3.orgs.Invitation`
645729 """
646- data = {}
647- if role in self .members_roles :
648- data ['role' ] = role
649- url = self ._build_url ('memberships' , username , base_url = self ._api )
650- return self ._json (self ._put (url , data = dumps (data )), 200 )
730+ if ((invitee_id is None and email is None ) or
731+ (invitee_id is not None and email is not None )):
732+ raise ValueError (
733+ "One of either 'invitee_id' or 'email' must be specified"
734+ )
735+ if not team_ids :
736+ raise ValueError (
737+ "'team_ids' must be a non-empty list of integers"
738+ )
739+ data = {'team_ids' : team_ids }
740+ if invitee_id is not None :
741+ data ['invitee_id' ] = invitee_id
742+ else :
743+ data ['email' ] = email
744+ if role not in self .invitation_roles :
745+ raise ValueError ("'role' must be one of {}" .format (', ' .join (
746+ sorted (self .invitation_roles )
747+ )))
748+ headers = {'Accept' : 'application/vnd.github.dazzler-preview.json' }
749+ data ['role' ] = role
750+ url = self ._build_url ('invitations' , base_url = self ._api )
751+ json = self ._json (self ._post (url , data = data , headers = headers ), 200 )
752+ return self ._instance_or_null (Invitation , json )
651753
652754 def is_member (self , username ):
653755 """Check if the user named ``username`` is a member.
@@ -734,14 +836,16 @@ def public_events(self, number=-1, etag=None):
734836
735837 @requires_auth
736838 def invitations (self , number = - 1 , etag = None ):
737- r """Iterate over outstanding invitations to this organization.
839+ """Iterate over outstanding invitations to this organization.
738840
739- :returns: generator of
841+ :returns:
842+ generator of invitation objects
843+ :rtype:
844+ :class:`~github3.orgs.Invitation`
740845 """
741- headers = {'Accept' : 'application/vnd.github.korra-preview' , }
742- params = {}
846+ headers = {'Accept' : 'application/vnd.github.korra-preview' }
743847 url = self ._build_url ('invitations' , base_url = self ._api )
744- return self ._iter (int (number ), url , dict , params = params , etag = etag ,
848+ return self ._iter (int (number ), url , Invitation , etag = etag ,
745849 headers = headers )
746850
747851 def members (self , filter = None , role = None , number = - 1 , etag = None ):
@@ -769,7 +873,7 @@ def members(self, filter=None, role=None, number=-1, etag=None):
769873 params = {}
770874 if filter in self .members_filters :
771875 params ['filter' ] = filter
772- if role in self .members_roles :
876+ if role in self .filterable_member_roles :
773877 params ['role' ] = role
774878 # TODO(sigmavirus24): Determine if the preview header is still
775879 # necessary
@@ -779,7 +883,7 @@ def members(self, filter=None, role=None, number=-1, etag=None):
779883 etag = etag , headers = headers )
780884
781885 @requires_auth
782- def membership (self , username ):
886+ def membership_for (self , username ):
783887 """Obtain the membership status of ``username``.
784888
785889 Implements
@@ -788,12 +892,13 @@ def membership(self, username):
788892 :param str username:
789893 (required), username name of the user
790894 :returns:
791- dictonary of the membership information
895+ the membership information
792896 :rtype:
793- dict
897+ :class:`~github3.orgs.Membership`
794898 """
795899 url = self ._build_url ('memberships' , username , base_url = self ._api )
796- return self ._json (self ._get (url ), 200 , 404 )
900+ json = self ._json (self ._get (url ), 200 , 404 )
901+ return self ._instance_or_null (Membership , json )
797902
798903 def public_members (self , number = - 1 , etag = None ):
799904 """Iterate over public members of this organization.
@@ -916,6 +1021,11 @@ def publicize_member(self, username):
9161021 def remove_member (self , username ):
9171022 """Remove the user named ``username`` from this organization.
9181023
1024+ .. note::
1025+
1026+ Only a user may publicize their own membership. See also:
1027+ https://developer.github.com/v3/orgs/members/#publicize-a-users-membership
1028+
9191029 :param str username:
9201030 name of the user to remove from the org
9211031 :returns:
@@ -1112,6 +1222,71 @@ class ShortOrganization(_Organization):
11121222 _refresh_to = Organization
11131223
11141224
1225+ class Invitation (models .GitHubCore ):
1226+ """Object representing an invitation to an organization.
1227+
1228+ .. attribute:: created_at
1229+
1230+ A :class:`~datetime.datetime` instance representing the time and date
1231+ when this invitation was created.
1232+
1233+ .. attribute:: email
1234+
1235+ The email address of the user invited to the organization.
1236+
1237+ .. attribute:: id
1238+
1239+ The unique identifier for this invitation.
1240+
1241+ .. attribute:: invitation_team_url
1242+
1243+ The API URL to retrieve the :class:`~github3.orgs.ShortTeam` objects
1244+ associated with this invitation.
1245+
1246+ .. attribute:: inviter
1247+
1248+ A :class:`~github3.users.ShortUser` representing the user who invited
1249+ the user identified by ``login``.
1250+
1251+ .. attribute:: login
1252+
1253+ The username of the user invited to the organization.
1254+
1255+ .. attribute:: team_count
1256+
1257+ The number of teams involved in this invitation.
1258+ """
1259+
1260+ def _update_attributes (self , json ):
1261+ self .created_at = self ._strptime (json ['created_at' ])
1262+ self .email = json ['email' ]
1263+ self .id = json ['id' ]
1264+ self .inviter = users .ShortUser (json ['inviter' ], self )
1265+ self .login = json ['login' ]
1266+ # NOTE(sigmavirus24): GitHub docs claim these should be present but
1267+ # in testing it is not.
1268+ self .invitation_team_url = json .get ('invitation_team_url' )
1269+ self .team_count = json .get ('team_count' )
1270+
1271+ def _repr (self ):
1272+ return '<Invitation {} for [{}] from [{}]>' .format (
1273+ self .id , self .login , self .inviter .login
1274+ )
1275+
1276+ @requires_auth
1277+ def teams (self ):
1278+ """Retrieve the list of teams associated with this invite.
1279+
1280+ :returns:
1281+ generator of teams associated with this invitation
1282+ :rtype:
1283+ :class:`~github3.orgs.ShortTeam`
1284+ """
1285+ return self ._iter (- 1 , self .invitation_team_url , ShortTeam , headers = {
1286+ 'Accept' : 'application/vnd.github.dazzler-preview.json' ,
1287+ })
1288+
1289+
11151290class Membership (models .GitHubCore ):
11161291 """Object describing a user's membership in teams and organizations.
11171292
0 commit comments