Repository: PyGithub/PyGithub

Issue:
Organization Outside Collaborators
https://developer.github.com/v3/orgs/outside_collaborators/

There is no matching method for this in PyGithub. I need this to make a chatops bot that polls who in an org has 2fa enabled. 

--timball


Discussion:

Resolving patch:
diff --git a/github/Organization.py b/github/Organization.py
--- a/github/Organization.py
+++ b/github/Organization.py
@@ -510,6 +510,49 @@ def get_public_members(self):
             None
         )
 
+    def get_outside_collaborators(self, filter_=github.GithubObject.NotSet):
+        """
+        :calls: `GET /orgs/:org/outside_collaborators <http://developer.github.com/v3/orgs/outside_collaborators>`_
+        :param filter_: string
+        :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.NamedUser.NamedUser`
+        """
+        assert (filter_ is github.GithubObject.NotSet or
+                isinstance(filter_, (str, unicode))), filter_
+
+        url_parameters = {}
+        if filter_ is not github.GithubObject.NotSet:
+            url_parameters["filter"] = filter_
+        return github.PaginatedList.PaginatedList(
+            github.NamedUser.NamedUser,
+            self._requester,
+            self.url + "/outside_collaborators",
+            url_parameters
+        )
+
+    def remove_outside_collaborator(self, collaborator):
+        """
+        :calls: `DELETE /orgs/:org/outside_collaborators/:username <https://developer.github.com/v3/orgs/outside_collaborators>`_
+        :param collaborator: :class:`github.NamedUser.NamedUser`
+        :rtype: None
+        """
+        assert isinstance(collaborator, github.NamedUser.NamedUser), collaborator
+        headers, data = self._requester.requestJsonAndCheck(
+            "DELETE",
+            self.url + "/outside_collaborators/" + collaborator._identity
+        )
+
+    def convert_to_outside_collaborator(self, member):
+        """
+        :calls: `PUT /orgs/:org/outside_collaborators/:username <https://developer.github.com/v3/orgs/outside_collaborators>`_
+        :param member: :class:`github.NamedUser.NamedUser`
+        :rtype: None
+        """
+        assert isinstance(member, github.NamedUser.NamedUser), member
+        headers, data = self._requester.requestJsonAndCheck(
+            "PUT",
+            self.url + "/outside_collaborators/" + member._identity
+        )
+
     def get_repo(self, name):
         """
         :calls: `GET /repos/:owner/:repo <http://developer.github.com/v3/repos>`_


Test patch:
diff --git a/github/tests/Organization.py b/github/tests/Organization.py
--- a/github/tests/Organization.py
+++ b/github/tests/Organization.py
@@ -108,6 +108,16 @@ def testGetIssuesWithAllArguments(self):
     def testGetMembers(self):
         self.assertListKeyEqual(self.org.get_members(), lambda u: u.login, ["cjuniet", "jacquev6", "Lyloa"])
 
+    def testGetOutsideCollaborators(self):
+        self.assertListKeyEqual(self.org.get_outside_collaborators(), lambda u: u.login, ["octocat"])
+
+    def testOutsideCollaborators(self):
+        octocat = self.g.get_user("octocat")
+        self.org.convert_to_outside_collaborator(octocat)
+        self.assertListKeyEqual(self.org.get_outside_collaborators(), lambda u: u.login, ["octocat"])
+        self.org.remove_outside_collaborator(octocat)
+        self.assertEqual(list(self.org.get_outside_collaborators()), [])
+
     def testMembers(self):
         lyloa = self.g.get_user("Lyloa")
         self.assertTrue(self.org.has_in_members(lyloa))
diff --git a/github/tests/ReplayData/Organization.testGetOutsideCollaborators.txt b/github/tests/ReplayData/Organization.testGetOutsideCollaborators.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Organization.testGetOutsideCollaborators.txt
@@ -0,0 +1,10 @@
+https
+GET
+api.github.com
+None
+/orgs/BeaverSoftware/outside_collaborators
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+null
+200
+[('status', '200 OK'), ('x-ratelimit-remaining', '4985'), ('content-length', '175'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"f7f3d1eaa1c0d14d590b09dbb439db2e"'), ('date', 'Sun, 28 Feb 2017 09:51:20 GMT'), ('content-type', 'application/json; charset=utf-8')]
+[{"login": "octocat","id": 1,"avatar_url": "https://github.com/images/error/octocat_happy.gif","gravatar_id": "","url": "https://api.github.com/users/octocat","html_url": "https://github.com/octocat","followers_url": "https://api.github.com/users/octocat/followers","following_url": "https://api.github.com/users/octocat/following{/other_user}","gists_url": "https://api.github.com/users/octocat/gists{/gist_id}","starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}","subscriptions_url": "https://api.github.com/users/octocat/subscriptions","organizations_url": "https://api.github.com/users/octocat/orgs","repos_url": "https://api.github.com/users/octocat/repos","events_url": "https://api.github.com/users/octocat/events{/privacy}","received_events_url": "https://api.github.com/users/octocat/received_events","type": "User","site_admin": false}]
diff --git a/github/tests/ReplayData/Organization.testOutsideCollaborators.txt b/github/tests/ReplayData/Organization.testOutsideCollaborators.txt
new file mode 100644
--- /dev/null
+++ b/github/tests/ReplayData/Organization.testOutsideCollaborators.txt
@@ -0,0 +1,54 @@
+https
+GET
+api.github.com
+None
+/users/octocat
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+null
+200
+[('status', '200 OK'), ('x-ratelimit-remaining', '4960'), ('content-length', '598'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"75740284d989e9a492e32f435cff48ac"'), ('date', 'Sat, 16 Apr 2018 20:23:33 GMT'), ('content-type', 'application/json; charset=utf-8')]
+{"type":"User","public_gists":19,"url":"https://api.github.com/users/octocat","hireable":false,"bio":null,"company":"Github","gravatar_id":"c5a7f21b46df698f3db31c37ed0cf55a","login":"octocat","blog":"http://blog.github.com","email":"info@github.com","public_repos":62,"followers":299,"name":"Octocat","created_at":"2009-05-12T21:19:38Z","location":"Netherlands","avatar_url":"","id":83844,"following":41,"html_url":"https://github.com/nvie"}
+
+https
+PUT
+api.github.com
+None
+/orgs/BeaverSoftware/outside_collaborators/octocat
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+null
+204
+[('status', '204 No Content'), ('x-ratelimit-remaining', '4992'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"d41d8cd98f00b204e9800998ecf8427e"'), ('date', 'Sun, 20 May 2012 12:47:52 GMT')]
+
+
+https
+GET
+api.github.com
+None
+/orgs/BeaverSoftware/outside_collaborators
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+null
+200
+[('status', '200 OK'), ('x-ratelimit-remaining', '4985'), ('content-length', '175'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"f7f3d1eaa1c0d14d590b09dbb439db2e"'), ('date', 'Sun, 28 Feb 2017 09:51:20 GMT'), ('content-type', 'application/json; charset=utf-8')]
+[{"login": "octocat","id": 1,"avatar_url": "https://github.com/images/error/octocat_happy.gif","gravatar_id": "","url": "https://api.github.com/users/octocat","html_url": "https://github.com/octocat","followers_url": "https://api.github.com/users/octocat/followers","following_url": "https://api.github.com/users/octocat/following{/other_user}","gists_url": "https://api.github.com/users/octocat/gists{/gist_id}","starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}","subscriptions_url": "https://api.github.com/users/octocat/subscriptions","organizations_url": "https://api.github.com/users/octocat/orgs","repos_url": "https://api.github.com/users/octocat/repos","events_url": "https://api.github.com/users/octocat/events{/privacy}","received_events_url": "https://api.github.com/users/octocat/received_events","type": "User","site_admin": false}]
+
+https
+DELETE
+api.github.com
+None
+/orgs/BeaverSoftware/outside_collaborators/octocat
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+null
+204
+[('status', '204 No Content'), ('x-ratelimit-remaining', '4992'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"d41d8cd98f00b204e9800998ecf8427e"'), ('date', 'Sun, 20 May 2012 12:47:52 GMT')]
+
+
+https
+GET
+api.github.com
+None
+/orgs/BeaverSoftware/outside_collaborators
+{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+null
+200
+[('status', '200 OK'), ('x-ratelimit-remaining', '4985'), ('content-length', '175'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"f7f3d1eaa1c0d14d590b09dbb439db2e"'), ('date', 'Sun, 28 Feb 2017 09:51:20 GMT'), ('content-type', 'application/json; charset=utf-8')]
+[]