Skip to content

Commit b5f8f07

Browse files
Add Support for GitHub Copilot Seat Management in Organizations (PyGithub#3082)
### Summary This pull request introduces support for managing GitHub Copilot seats in organizations. It adds new classes and methods to handle Copilot-related API calls, such as retrieving all seats, adding new seats, and removing seats for specific users. ### Motivation The addition of GitHub Copilot seat management functionality extends PyGithub to include support for the Copilot Business API. This enhancement enables users to automate and manage Copilot seats within their organizations programmatically, improving workflow efficiency. ### Changes - **New Classes**: - `Copilot`: Represents the Copilot entity and provides methods to interact with Copilot-related endpoints. - `CopilotSeat`: Represents individual Copilot seats and their attributes, such as assignee, last activity, and plan type. - **New Methods**: - `Organization.get_copilot`: Retrieves a `Copilot` object for the organization. - `Copilot.get_seats`: Fetches all Copilot seats for an organization. - `Copilot.add_seats`: Adds seats for selected usernames. - `Copilot.remove_seats`: Removes seats for selected usernames. --------- Co-authored-by: Enrico Minack <[email protected]>
1 parent f23da45 commit b5f8f07

File tree

9 files changed

+308
-0
lines changed

9 files changed

+308
-0
lines changed

github/Copilot.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
############################ Copyrights and license ############################
2+
# #
3+
# Copyright 2024 Pasha Fateev <[email protected]> #
4+
# #
5+
# This file is part of PyGithub. #
6+
# http://pygithub.readthedocs.io/ #
7+
# #
8+
# PyGithub is free software: you can redistribute it and/or modify it under #
9+
# the terms of the GNU Lesser General Public License as published by the Free #
10+
# Software Foundation, either version 3 of the License, or (at your option) #
11+
# any later version. #
12+
# #
13+
# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY #
14+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
15+
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more #
16+
# details. #
17+
# #
18+
# You should have received a copy of the GNU Lesser General Public License #
19+
# along with PyGithub. If not, see <http://www.gnu.org/licenses/>. #
20+
# #
21+
################################################################################
22+
23+
from __future__ import annotations
24+
25+
from typing import TYPE_CHECKING, Any
26+
27+
import github.CopilotSeat
28+
from github.GithubObject import Attribute, NonCompletableGithubObject, NotSet
29+
from github.PaginatedList import PaginatedList
30+
31+
if TYPE_CHECKING:
32+
from github.CopilotSeat import CopilotSeat
33+
from github.Requester import Requester
34+
35+
36+
class Copilot(NonCompletableGithubObject):
37+
def __init__(self, requester: Requester, org_name: str) -> None:
38+
super().__init__(requester, {}, {"org_name": org_name})
39+
40+
def _initAttributes(self) -> None:
41+
self._org_name: Attribute[str] = NotSet
42+
43+
def _useAttributes(self, attributes: dict[str, Any]) -> None:
44+
if "org_name" in attributes: # pragma no branch
45+
self._org_name = self._makeStringAttribute(attributes["org_name"])
46+
47+
def __repr__(self) -> str:
48+
return self.get__repr__({"org_name": self._org_name.value if self._org_name is not NotSet else NotSet})
49+
50+
@property
51+
def org_name(self) -> str:
52+
return self._org_name.value
53+
54+
def get_seats(self) -> PaginatedList[CopilotSeat]:
55+
"""
56+
:calls: `GET /orgs/{org}/copilot/billing/seats <https://docs.github.com/en/rest/copilot/copilot-business>`_
57+
"""
58+
url = f"/orgs/{self._org_name.value}/copilot/billing/seats"
59+
return PaginatedList(
60+
github.CopilotSeat.CopilotSeat,
61+
self._requester,
62+
url,
63+
None,
64+
list_item="seats",
65+
)
66+
67+
def add_seats(self, selected_usernames: list[str]) -> int:
68+
"""
69+
:calls: `POST /orgs/{org}/copilot/billing/selected_users <https://docs.github.com/en/rest/copilot/copilot-business>`_
70+
:param selected_usernames: List of usernames to add Copilot seats for
71+
:rtype: int
72+
:return: Number of seats created
73+
"""
74+
url = f"/orgs/{self._org_name.value}/copilot/billing/selected_users"
75+
_, data = self._requester.requestJsonAndCheck(
76+
"POST",
77+
url,
78+
input={"selected_usernames": selected_usernames},
79+
)
80+
return data["seats_created"]
81+
82+
def remove_seats(self, selected_usernames: list[str]) -> int:
83+
"""
84+
:calls: `DELETE /orgs/{org}/copilot/billing/selected_users <https://docs.github.com/en/rest/copilot/copilot-business>`_
85+
:param selected_usernames: List of usernames to remove Copilot seats for
86+
:rtype: int
87+
:return: Number of seats cancelled
88+
"""
89+
url = f"/orgs/{self._org_name.value}/copilot/billing/selected_users"
90+
_, data = self._requester.requestJsonAndCheck(
91+
"DELETE",
92+
url,
93+
input={"selected_usernames": selected_usernames},
94+
)
95+
return data["seats_cancelled"]

github/CopilotSeat.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
############################ Copyrights and license ############################
2+
# #
3+
# Copyright 2024 Pasha Fateev <[email protected]> #
4+
# #
5+
# This file is part of PyGithub. #
6+
# http://pygithub.readthedocs.io/ #
7+
# #
8+
# PyGithub is free software: you can redistribute it and/or modify it under #
9+
# the terms of the GNU Lesser General Public License as published by the Free #
10+
# Software Foundation, either version 3 of the License, or (at your option) #
11+
# any later version. #
12+
# #
13+
# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY #
14+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
15+
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more #
16+
# details. #
17+
# #
18+
# You should have received a copy of the GNU Lesser General Public License #
19+
# along with PyGithub. If not, see <http://www.gnu.org/licenses/>. #
20+
# #
21+
################################################################################
22+
23+
from __future__ import annotations
24+
25+
from datetime import datetime
26+
from typing import Any
27+
28+
import github.NamedUser
29+
import github.Team
30+
from github.GithubObject import Attribute, NonCompletableGithubObject, NotSet, _NotSetType
31+
32+
33+
class CopilotSeat(NonCompletableGithubObject):
34+
def _initAttributes(self) -> None:
35+
self._created_at: Attribute[datetime] | _NotSetType = NotSet
36+
self._updated_at: Attribute[datetime] | _NotSetType = NotSet
37+
self._pending_cancellation_date: Attribute[datetime] | _NotSetType = NotSet
38+
self._last_activity_at: Attribute[datetime] | _NotSetType = NotSet
39+
self._last_activity_editor: Attribute[str] | _NotSetType = NotSet
40+
self._plan_type: Attribute[str] | _NotSetType = NotSet
41+
self._assignee: Attribute[github.NamedUser.NamedUser] | _NotSetType = NotSet
42+
self._assigning_team: Attribute[github.Team.Team] | _NotSetType = NotSet
43+
44+
def _useAttributes(self, attributes: dict[str, Any]) -> None:
45+
if "created_at" in attributes:
46+
self._created_at = self._makeDatetimeAttribute(attributes["created_at"])
47+
if "updated_at" in attributes:
48+
self._updated_at = self._makeDatetimeAttribute(attributes["updated_at"])
49+
if "pending_cancellation_date" in attributes:
50+
self._pending_cancellation_date = self._makeDatetimeAttribute(attributes["pending_cancellation_date"])
51+
if "last_activity_at" in attributes:
52+
self._last_activity_at = self._makeDatetimeAttribute(attributes["last_activity_at"])
53+
if "last_activity_editor" in attributes:
54+
self._last_activity_editor = self._makeStringAttribute(attributes["last_activity_editor"])
55+
if "plan_type" in attributes:
56+
self._plan_type = self._makeStringAttribute(attributes["plan_type"])
57+
if "assignee" in attributes:
58+
self._assignee = self._makeClassAttribute(github.NamedUser.NamedUser, attributes["assignee"])
59+
if "assigning_team" in attributes:
60+
self._assigning_team = self._makeClassAttribute(github.Team.Team, attributes["assigning_team"])
61+
62+
def __repr__(self) -> str:
63+
return self.get__repr__({"assignee": self._assignee.value})
64+
65+
@property
66+
def created_at(self) -> datetime:
67+
return self._created_at.value
68+
69+
@property
70+
def updated_at(self) -> datetime:
71+
return self._updated_at.value
72+
73+
@property
74+
def pending_cancellation_date(self) -> datetime:
75+
return self._pending_cancellation_date.value
76+
77+
@property
78+
def last_activity_at(self) -> datetime:
79+
return self._last_activity_at.value
80+
81+
@property
82+
def last_activity_editor(self) -> str:
83+
return self._last_activity_editor.value
84+
85+
@property
86+
def plan_type(self) -> str:
87+
return self._plan_type.value
88+
89+
@property
90+
def assignee(self) -> github.NamedUser.NamedUser:
91+
return self._assignee.value
92+
93+
@property
94+
def assigning_team(self) -> github.Team.Team:
95+
return self._assigning_team.value

github/Organization.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
from datetime import datetime
8787
from typing import TYPE_CHECKING, Any
8888

89+
import github.Copilot
8990
import github.Event
9091
import github.GithubObject
9192
import github.HookDelivery
@@ -112,6 +113,7 @@
112113
from github.PaginatedList import PaginatedList
113114

114115
if TYPE_CHECKING:
116+
from github.Copilot import Copilot
115117
from github.Event import Event
116118
from github.Hook import Hook
117119
from github.Installation import Installation
@@ -1053,6 +1055,12 @@ def get_public_key(self, secret_type: str = "actions") -> PublicKey:
10531055
headers, data = self._requester.requestJsonAndCheck("GET", f"{self.url}/{secret_type}/secrets/public-key")
10541056
return github.PublicKey.PublicKey(self._requester, headers, data, completed=True)
10551057

1058+
def get_copilot(self) -> Copilot:
1059+
"""
1060+
:calls: Various Copilot-related endpoints for this organization :rtype: :class:`github.Copilot.Copilot`
1061+
"""
1062+
return github.Copilot.Copilot(self._requester, self.login)
1063+
10561064
def get_repo(self, name: str) -> Repository:
10571065
"""
10581066
:calls: `GET /repos/{owner}/{repo} <https://docs.github.com/en/rest/reference/repos>`_

tests/Copilot.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
############################ Copyrights and license ############################
2+
# #
3+
# Copyright 2024 Pasha Fateev <[email protected]> #
4+
# #
5+
# This file is part of PyGithub. #
6+
# http://pygithub.readthedocs.io/ #
7+
# #
8+
# PyGithub is free software: you can redistribute it and/or modify it under #
9+
# the terms of the GNU Lesser General Public License as published by the Free #
10+
# Software Foundation, either version 3 of the License, or (at your option) #
11+
# any later version. #
12+
# #
13+
# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY #
14+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
15+
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more #
16+
# details. #
17+
# #
18+
# You should have received a copy of the GNU Lesser General Public License #
19+
# along with PyGithub. If not, see <http://www.gnu.org/licenses/>. #
20+
# #
21+
################################################################################
22+
23+
from datetime import datetime, timezone
24+
25+
from . import Framework
26+
27+
28+
class Copilot(Framework.TestCase):
29+
def setUp(self):
30+
super().setUp()
31+
self.org_name = "BeaverSoftware"
32+
self.copilot = self.g.get_organization(self.org_name).get_copilot()
33+
34+
def testAttributes(self):
35+
self.assertEqual(self.copilot.org_name, "BeaverSoftware")
36+
self.assertEqual(repr(self.copilot), 'Copilot(org_name="BeaverSoftware")')
37+
38+
seats = list(self.copilot.get_seats())
39+
self.assertEqual(len(seats), 1)
40+
seat = seats[0]
41+
self.assertEqual(seat.created_at, datetime(2010, 7, 9, 6, 10, 6, tzinfo=timezone.utc))
42+
self.assertEqual(seat.updated_at, datetime(2012, 5, 26, 11, 25, 48, tzinfo=timezone.utc))
43+
self.assertEqual(seat.pending_cancellation_date, None)
44+
self.assertEqual(seat.last_activity_at, datetime(2012, 5, 26, 14, 59, 39, tzinfo=timezone.utc))
45+
self.assertEqual(seat.last_activity_editor, "vscode/1.0.0")
46+
self.assertEqual(seat.plan_type, "business")
47+
self.assertEqual(seat.assignee.login, "pashafateev")
48+
self.assertEqual(repr(seat), 'CopilotSeat(assignee=NamedUser(login="pashafateev"))')
49+
50+
def testGetSeats(self):
51+
seats = self.copilot.get_seats()
52+
self.assertListKeyEqual(seats, lambda s: s.assignee.login, ["pashafateev"])
53+
54+
def testAddSeats(self):
55+
seats_created = self.copilot.add_seats(["pashafateev"])
56+
self.assertEqual(seats_created, 1)
57+
58+
def testRemoveSeats(self):
59+
seats_cancelled = self.copilot.remove_seats(["pashafateev"])
60+
self.assertEqual(seats_cancelled, 1)

tests/ReplayData/Copilot.setUp.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
https
2+
GET
3+
api.github.com
4+
None
5+
/orgs/BeaverSoftware
6+
{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
7+
None
8+
200
9+
[('status', '200 OK'), ('x-ratelimit-remaining', '4976'), ('content-length', '716'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"4862bcec9fa538316e2fcd73be37b846"'), ('date', 'Sat, 26 May 2012 21:09:52 GMT'), ('content-type', 'application/json; charset=utf-8')]
10+
{"type":"Organization","login":"BeaverSoftware","id":1424031}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
https
2+
POST
3+
api.github.com
4+
None
5+
/orgs/BeaverSoftware/copilot/billing/selected_users
6+
{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'}
7+
{"selected_usernames": ["pashafateev"]}
8+
201
9+
[('status', '201 Created'), ('x-ratelimit-remaining', '4975'), ('content-length', '19'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"4862bcec9fa538316e2fcd73be37b846"'), ('date', 'Sat, 26 May 2012 21:09:52 GMT'), ('content-type', 'application/json; charset=utf-8')]
10+
{"seats_created":1}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
https
2+
GET
3+
api.github.com
4+
None
5+
/orgs/BeaverSoftware/copilot/billing/seats
6+
{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
7+
None
8+
200
9+
[('status', '200 OK'), ('x-ratelimit-remaining', '4973'), ('content-length', '716'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"4862bcec9fa538316e2fcd73be37b846"'), ('date', 'Sat, 26 May 2012 21:09:52 GMT'), ('content-type', 'application/json; charset=utf-8')]
10+
{"seats": [{"created_at": "2010-07-09T06:10:06Z", "updated_at": "2012-05-26T11:25:48Z", "pending_cancellation_date": null, "last_activity_at": "2012-05-26T14:59:39Z", "last_activity_editor": "vscode/1.0.0", "plan_type": "business", "assignee": {"login": "pashafateev", "id": 327146}}]}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
https
2+
GET
3+
api.github.com
4+
None
5+
/orgs/BeaverSoftware/copilot/billing/seats
6+
{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
7+
None
8+
200
9+
[('status', '200 OK'), ('x-ratelimit-remaining', '4973'), ('content-length', '716'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"4862bcec9fa538316e2fcd73be37b846"'), ('date', 'Sat, 26 May 2012 21:09:52 GMT'), ('content-type', 'application/json; charset=utf-8')]
10+
{"seats": [{"created_at": "2010-07-09T06:10:06Z", "updated_at": "2012-05-26T11:25:48Z", "pending_cancellation_date": null, "last_activity_at": "2012-05-26T14:59:39Z", "last_activity_editor": "vscode/1.0.0", "plan_type": "business", "assignee": {"login": "pashafateev", "id": 327146}}]}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
https
2+
DELETE
3+
api.github.com
4+
None
5+
/orgs/BeaverSoftware/copilot/billing/selected_users
6+
{'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python', 'Content-Type': 'application/json'}
7+
{"selected_usernames": ["pashafateev"]}
8+
200
9+
[('status', '200 OK'), ('x-ratelimit-remaining', '4972'), ('content-length', '21'), ('server', 'nginx/1.0.13'), ('connection', 'keep-alive'), ('x-ratelimit-limit', '5000'), ('etag', '"4862bcec9fa538316e2fcd73be37b846"'), ('date', 'Sat, 26 May 2012 21:09:52 GMT'), ('content-type', 'application/json; charset=utf-8')]
10+
{"seats_cancelled":1}

0 commit comments

Comments
 (0)