Repository: PyGithub/PyGithub

Issue:
Inviting a user via email address using pygithub.
Hi Team,
Have gone through the documentation and found solutions to invite a nameduser. I have use case where i wanted to invite an external user using his E-mail address. But could not see an option to do the same using pygithub where only nameduser is supported.

Kindly help. 


Discussion:
You are right. I guess the following endpoint is yet to be implemented:
https://developer.github.com/v3/orgs/members/#create-organization-invitation



Resolving patch:
diff --git a/github/Consts.py b/github/Consts.py
--- a/github/Consts.py
+++ b/github/Consts.py
@@ -81,5 +81,8 @@
 # https://developer.github.com/changes/2018-01-10-lock-reason-api-preview/
 mediaTypeLockReasonPreview = "application/vnd.github.sailor-v-preview+json"
 
+# https://developer.github.com/changes/2018-01-25-organization-invitation-api-preview/
+mediaTypeOrganizationInvitationPreview = "application/vnd.github.dazzler-preview+json"
+
 # https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/
 mediaTypeRequireMultipleApprovingReviews = "application/vnd.github.luke-cage-preview+json"
diff --git a/github/Organization.py b/github/Organization.py
--- a/github/Organization.py
+++ b/github/Organization.py
@@ -21,6 +21,7 @@
 # Copyright 2018 Raihaan <31362124+res0nance@users.noreply.github.com>         #
 # Copyright 2018 Tim Boring <tboring@hearst.com>                               #
 # Copyright 2018 sfdye <tsfdye@gmail.com>                                      #
+# Copyright 2018 Steve Kowalik <steven@wedontsleep.org>                        #
 #                                                                              #
 # This file is part of PyGithub.                                               #
 # http://pygithub.readthedocs.io/                                              #
@@ -52,6 +53,7 @@
 import github.Repository
 import github.NamedUser
 
+import Consts
 
 class Organization(github.GithubObject.CompletableGithubObject):
     """
@@ -776,6 +778,37 @@ def get_teams(self):
             None
         )
 
+    def invite_user(self, user=github.GithubObject.NotSet, email=github.GithubObject.NotSet, role=github.GithubObject.NotSet, teams=github.GithubObject.NotSet):
+        """
+        :calls: `POST /orgs/:org/invitations <http://developer.github.com/v3/orgs/members>`_
+        :param user: :class:`github.NamedUser.NamedUser`
+        :param email: string
+        :param role: string
+        :param teams: array of :class:`github.Team.Team`
+        :rtype: None
+        """
+        assert user is github.GithubObject.NotSet or isinstance(user, github.NamedUser.NamedUser), user
+        assert email is github.GithubObject.NotSet or isinstance(email, (str, unicode)), email
+        assert (email is github.GithubObject.NotSet) ^ (user is github.GithubObject.NotSet), "specify only one of email or user"
+        parameters = {}
+        if user is not github.GithubObject.NotSet:
+            parameters["invitee_id"] = user.id
+        elif email is not github.GithubObject.NotSet:
+            parameters["email"] = email
+        if role is not github.GithubObject.NotSet:
+            assert isinstance(role, (str, unicode)), role
+            assert role in ['admin', 'direct_member', 'billing_manager']
+            parameters["role"] = role
+        if teams is not github.GithubObject.NotSet:
+            assert all(isinstance(team, github.Team.Team) for team in teams)
+            parameters["team_ids"] = [t.id for t in teams]
+        headers, data = self._requester.requestJsonAndCheck(
+            "POST",
+            self.url + "/invitations",
+            headers={'Accept': Consts.mediaTypeOrganizationInvitationPreview},
+            input=parameters
+        )
+
     def has_in_members(self, member):
         """
         :calls: `GET /orgs/:org/members/:user <http://developer.github.com/v3/orgs/members>`_


Test patch:
diff --git a/github/tests/Organization.py b/github/tests/Organization.py
--- a/github/tests/Organization.py
+++ b/github/tests/Organization.py
@@ -34,6 +34,8 @@
 #                                                                              #
 ################################################################################
 
+import github
+
 import Framework
 
 import datetime
@@ -213,3 +215,35 @@ def testCreateFork(self):
         pygithub = self.g.get_user("jacquev6").get_repo("PyGithub")
         repo = self.org.create_fork(pygithub)
         self.assertEqual(repo.url, "https://api.github.com/repos/BeaverSoftware/PyGithub")
+
+    def testInviteUserWithNeither(self):
+        with self.assertRaises(AssertionError) as raisedexp:
+            self.org.invite_user()
+        self.assertEqual("specify only one of email or user", str(raisedexp.exception))
+
+    def testInviteUserWithBoth(self):
+        jacquev6 = self.g.get_user('jacquev6')
+        with self.assertRaises(AssertionError) as raisedexp:
+            self.org.invite_user(email='foo', user=jacquev6)
+        self.assertEqual("specify only one of email or user", str(raisedexp.exception))
+
+    def testInviteUserByName(self):
+        jacquev6 = self.g.get_user('jacquev6')
+        self.org.invite_user(user=jacquev6)
+
+    def testInviteUserByEmail(self):
+        self.org.invite_user(email='foo@example.com')
+
+    def testInviteUserWithRoleAndTeam(self):
+        team = self.org.create_team("Team created by PyGithub")
+        self.org.invite_user(email='foo@example.com', role='billing_manager', teams=[team])
+
+    def testInviteUserAsNonOwner(self):
+        with self.assertRaises(github.GithubException) as raisedexp:
+            self.org.invite_user(email='bar@example.com')
+        self.assertEqual(raisedexp.exception.status, 403)
+        self.assertEqual(raisedexp.exception.data, {
+            u'documentation_url': u'https://developer.github.com/v3/orgs/members/#create-organization-invitation',
+            u'message': u'You must be an admin to create an invitation to an organization.'
+            }
+        )
diff --git a/github/tests/ReplayData/Organization.testInviteUserAsNonOwner.txt b/github/tests/ReplayData/Organization.testInviteUserAsNonOwner.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Organization.testInviteUserAsNonOwner.txt
@@ -0,0 +1,11 @@
+https
+POST
+api.github.com
+None
+/orgs/BeaverSoftware/invitations
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.dazzler-preview+json'}
+{"email": "bar@example.com"}
+403
+[('status', '403 Forbidden'), ('x-ratelimit-remaining', '4980'), ('content-length', '37'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"fb43ecb6e2f75e3940aa8e9edc5ed691"'), ('date', 'Sun, 26 Aug 2018 01:19:54 GMT'), ('content-type', 'application/json; charset=utf-8')]
+{"documentation_url": "https://developer.github.com/v3/orgs/members/#create-organization-invitation", "message": "You must be an admin to create an invitation to an organization."}
+
diff --git a/github/tests/ReplayData/Organization.testInviteUserByEmail.txt b/github/tests/ReplayData/Organization.testInviteUserByEmail.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Organization.testInviteUserByEmail.txt
@@ -0,0 +1,10 @@
+https
+POST
+api.github.com
+None
+/orgs/BeaverSoftware/invitations
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json', 'Accept': 'application/vnd.github.dazzler-preview+json'}
+{"email": "foo@example.com"}
+201
+[('content-length', '1273'), ('x-runtime-rack', '0.071768'), ('vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding'), ('x-oauth-scopes', 'admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, write:discussion'), ('x-xss-protection', '1; mode=block'), ('x-content-type-options', 'nosniff'), ('x-accepted-oauth-scopes', 'admin:org'), ('etag', '"a7e7e69aa42ed0d2132dbfcaa032dbf7"'), ('cache-control', 'private, max-age=60, s-maxage=60'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('status', '201 Created'), ('x-ratelimit-remaining', '4998'), ('x-github-media-type', 'github.dazzler-preview; format=json'), ('access-control-expose-headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('x-github-request-id', 'A542:171F:1945733:1F92438:5B822DC1'), ('date', 'Sun, 26 Aug 2018 04:34:24 GMT'), ('access-control-allow-origin', '*'), ('content-security-policy', "default-src 'none'"), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('server', 'GitHub.com'), ('x-ratelimit-limit', '5000'), ('x-frame-options', 'deny'), ('content-type', 'application/json; charset=utf-8'), ('x-ratelimit-reset', '1535261649')]
+{"created_at":"2018-08-26T04:15:26Z","email":"foo@example.com","invitation_teams_url":"https://api.github.com/organizations/42690536/invitations/12423419/teams","node_id":"MDIyOk9yZ2FuaXphdGlvbkludml0YXRpb24xMjQyMzQxOQ==","role":"direct_member","team_count":0,"login":null,"inviter":{"following_url":"https://api.github.com/users/jacquev6/following{/other_user}","events_url":"https://api.github.com/users/jacquev6/events{/privacy}","avatar_url":"https://avatars0.githubusercontent.com/u/327146?v=4","url":"https://api.github.com/users/jacquev6","gists_url":"https://api.github.com/users/jacquev6/gists{/gist_id}","html_url":"https://github.com/jacquev6","subscriptions_url":"https://api.github.com/users/jacquev6/subscriptions","node_id":"MDQ6VXNlcjE1MjI1MDU5","repos_url":"https://api.github.com/users/jacquev6/repos","received_events_url":"https://api.github.com/users/jacquev6/received_events","gravatar_id":"","starred_url":"https://api.github.com/users/jacquev6/starred{/owner}{/repo}","site_admin":false,"login":"jacquev6","type":"User","id":327146,"followers_url":"https://api.github.com/users/jacquev6/followers","organizations_url":"https://api.github.com/users/jacquev6/orgs"},"id":12423419}
diff --git a/github/tests/ReplayData/Organization.testInviteUserByName.txt b/github/tests/ReplayData/Organization.testInviteUserByName.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Organization.testInviteUserByName.txt
@@ -0,0 +1,21 @@
+https
+GET
+api.github.com
+None
+/users/jacquev6
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+200
+[('status', '200 OK'), ('x-ratelimit-remaining', '4962'), ('content-length', '801'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"fc78d67f262cad756e42354c78ecea4e"'), ('date', 'Tue, 28 Aug 2018 00:16:42 GMT'), ('content-type', 'application/json; charset=utf-8')]
+{"public_repos":11,"type":"User","disk_usage":17080,"hireable":false,"blog":"http://vincent-jacques.net","url":"https://api.github.com/users/jacquev6","bio":"","plan":{"collaborators":1,"private_repos":5,"name":"micro","space":614400},"avatar_url":"https://secure.gravatar.com/avatar/b68de5ae38616c296fa345d2b9df2225?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png","total_private_repos":5,"public_gists":2,"company":"Criteo","gravatar_id":"b68de5ae38616c296fa345d2b9df2225","login":"jacquev6","owned_private_repos":5,"private_gists":5,"collaborators":0,"email":"vincent@vincent-jacques.net","followers":13,"name":"Vincent Jacques","created_at":"2010-07-09T06:10:06Z","location":"Paris, France","id":327146,"following":24,"html_url":"https://github.com/jacquev6"}
+
+https
+POST
+api.github.com
+None
+/orgs/BeaverSoftware/invitations
+{'Authorization': 'Basic login_and_password_removed', 'Content-Type': 'application/json', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.dazzler-preview+json'}
+{"invitee_id": 327146}
+201
+[('content-length', '1275'), ('x-runtime-rack', '0.117213'), ('vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding'), ('x-oauth-scopes', 'admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, write:discussion'), ('x-xss-protection', '1; mode=block'), ('x-content-type-options', 'nosniff'), ('x-accepted-oauth-scopes', 'admin:org'), ('etag', '"25cb4d8e0275889661b12ee12785894c"'), ('cache-control', 'private, max-age=60, s-maxage=60'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('status', '201 Created'), ('x-ratelimit-remaining', '4990'), ('x-github-media-type', 'github.dazzler-preview; format=json'), ('access-control-expose-headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('x-github-request-id', 'EA96:5917:ED3378:133B5C8:5B849484'), ('date', 'Tue, 28 Aug 2018 00:17:45 GMT'), ('access-control-allow-origin', '*'), ('content-security-policy', "default-src 'none'"), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('server', 'GitHub.com'), ('x-ratelimit-limit', '5000'), ('x-frame-options', 'deny'), ('content-type', 'application/json; charset=utf-8'), ('x-ratelimit-reset', '1535417380')]
+{"created_at":"2018-08-28T00:17:45Z","email":"vincent@vincent-jacques.net","invitation_teams_url":"https://api.github.com/organizations/42733195/invitations/12434153/teams","node_id":"MDIyOk9yZ2FuaXphdGlvbkludml0YXRpb24xMjQzNDE1Mw==","role":"direct_member","team_count":0,"login":"jacquev6","inviter":{"following_url":"https://api.github.com/users/jacquev6/following{/other_user}","events_url":"https://api.github.com/users/jacquev6/events{/privacy}","avatar_url":"https://avatars0.githubusercontent.com/u/327146?v=4","url":"https://api.github.com/users/jacquev6","gists_url":"https://api.github.com/users/jacquev6/gists{/gist_id}","html_url":"https://github.com/jacquev6","subscriptions_url":"https://api.github.com/users/jacquev6/subscriptions","node_id":"MDQ6VXNlcjE1MjI1MDU5","repos_url":"https://api.github.com/users/jacquev6/repos","received_events_url":"https://api.github.com/users/jacquev6/received_events","gravatar_id":"","starred_url":"https://api.github.com/users/jacquev6/starred{/owner}{/repo}","site_admin":false,"login":"jacquev6","type":"User","id":327146,"followers_url":"https://api.github.com/users/jacquev6/followers","organizations_url":"https://api.github.com/users/jacquev6/orgs"},"id":12434153}
diff --git a/github/tests/ReplayData/Organization.testInviteUserWithBoth.txt b/github/tests/ReplayData/Organization.testInviteUserWithBoth.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Organization.testInviteUserWithBoth.txt
@@ -0,0 +1,10 @@
+https
+GET
+api.github.com
+None
+/users/jacquev6
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+None
+200
+[('status', '200 OK'), ('x-ratelimit-remaining', '4962'), ('content-length', '801'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"fc78d67f262cad756e42354c78ecea4e"'), ('date', 'Tue, 28 Aug 2018 00:16:42 GMT'), ('content-type', 'application/json; charset=utf-8')]
+{"public_repos":11,"type":"User","disk_usage":17080,"hireable":false,"blog":"http://vincent-jacques.net","url":"https://api.github.com/users/jacquev6","bio":"","plan":{"collaborators":1,"private_repos":5,"name":"micro","space":614400},"avatar_url":"https://secure.gravatar.com/avatar/b68de5ae38616c296fa345d2b9df2225?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png","total_private_repos":5,"public_gists":2,"company":"Criteo","gravatar_id":"b68de5ae38616c296fa345d2b9df2225","login":"jacquev6","owned_private_repos":5,"private_gists":5,"collaborators":0,"email":"vincent@vincent-jacques.net","followers":13,"name":"Vincent Jacques","created_at":"2010-07-09T06:10:06Z","location":"Paris, France","id":327146,"following":24,"html_url":"https://github.com/jacquev6"}
diff --git a/github/tests/ReplayData/Organization.testInviteUserWithRoleAndTeam.txt b/github/tests/ReplayData/Organization.testInviteUserWithRoleAndTeam.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Organization.testInviteUserWithRoleAndTeam.txt
@@ -0,0 +1,21 @@
+https
+POST
+api.github.com
+None
+/orgs/BeaverSoftware/teams
+{'Content-Type': 'application/json', 'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+{"name": "Team created by PyGithub"}
+201
+[('status', '201 Created'), ('x-ratelimit-remaining', '4988'), ('content-length', '145'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"189a318993cde3e040f2efb4f634f8a8"'), ('date', 'Sat, 26 May 2012 20:58:53 GMT'), ('content-type', 'application/json; charset=utf-8'), ('location', 'https://api.github.com/teams/189850')]
+{"url":"https://api.github.com/teams/189850","members_count":0,"repos_count":0,"name":"Team created by PyGithub","permission":"pull","id":189850}
+
+https
+POST
+api.github.com
+None
+/orgs/BeaverSoftware/invitations
+{'Content-Type': 'application/json', 'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python', 'Accept': 'application/vnd.github.dazzler-preview+json'}
+{"email": "foo@example.com", "role": "billing_manager", "team_ids": [189850]}
+201
+[('content-length', '1278'), ('x-runtime-rack', '0.122927'), ('vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding'), ('x-oauth-scopes', 'admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, delete_repo, gist, notifications, repo, user, write:discussion'), ('x-xss-protection', '1; mode=block'), ('x-content-type-options', 'nosniff'), ('x-accepted-oauth-scopes', 'admin:org'), ('etag', '"956a9bf38063ecf225e5970155c6022a"'), ('cache-control', 'private, max-age=60, s-maxage=60'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('status', '201 Created'), ('x-ratelimit-remaining', '4994'), ('x-github-media-type', 'github.dazzler-preview; format=json'), ('access-control-expose-headers', 'ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval'), ('x-github-request-id', 'DA68:171E:16B488C:1C7BDFC:5B8232A3'), ('date', 'Sun, 26 Aug 2018 04:55:00 GMT'), ('access-control-allow-origin', '*'), ('content-security-policy', "default-src 'none'"), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('server', 'GitHub.com'), ('x-ratelimit-limit', '5000'), ('x-frame-options', 'deny'), ('content-type', 'application/json; charset=utf-8'), ('x-ratelimit-reset', '1535261649')]
+{"created_at":"2018-08-26T04:55:00Z","email":"foo@example.com","invitation_teams_url":"https://api.github.com/organizations/32695636/invitations/12325953/teams","node_id":"MDIyOk9yZ2FuaXphdGlvbkludml0YXRpb24xMjQyMzQ1Mw==","role":"billing_manager","team_count":1,"login":null,"inviter":{"following_url":"https://api.github.com/users/jacquev6/following{/other_user}","events_url":"https://api.github.com/users/jacquev6/events{/privacy}","avatar_url":"https://avatars0.githubusercontent.com/u/327146?v=4","url":"https://api.github.com/users/jacquev6","gists_url":"https://api.github.com/users/jacquev6/gists{/gist_id}","html_url":"https://github.com/jacquev6","subscriptions_url":"https://api.github.com/users/jacquev6/subscriptions","node_id":"MDQ6VXNlcjE1MjI1MDU5","repos_url":"https://api.github.com/users/jacquev6/repos","received_events_url":"https://api.github.com/users/jacquev6/received_events","gravatar_id":"","starred_url":"https://api.github.com/users/jacquev6/starred{/owner}{/repo}","site_admin":false,"login":"jacquev6","type":"User","id":327146,"followers_url":"https://api.github.com/users/jacquev6/followers","organizations_url":"https://api.github.com/users/jacquev6/orgs"},"id":12423453}