Skip to content

Commit

Permalink
Update to avoid /teams deprecation
Browse files Browse the repository at this point in the history
GitHub deprecated the top-level /teams API in 2020 and is starting to
run brown-outs with the goal of removing on March 15.

Closes #1080
  • Loading branch information
sigmavirus24 committed Mar 2, 2022
1 parent 38b6f88 commit 53f9718
Show file tree
Hide file tree
Showing 17 changed files with 129 additions and 81 deletions.
15 changes: 7 additions & 8 deletions docs/source/release-notes/3.2.0.rst
@@ -1,13 +1,12 @@
3.2.0: 2022-xx-xx
3.2.0: 2022-03-02
-----------------

Dependency Change
`````````````````

Bug Fixes
`````````

Features Added
``````````````
- Migrate to GitHub's supported Teams endpoints for select methods that were
relying on the deprecated endpoints. See also gh-1080_


Bug Fixes
`````````
.. _gh-1080:
https://github.com/sigmavirus24/github3.py/issues/1080
2 changes: 1 addition & 1 deletion src/github3/__about__.py
Expand Up @@ -5,7 +5,7 @@
__author_email__ = "graffatcolmingov@gmail.com"
__license__ = "Modified BSD"
__copyright__ = "Copyright 2012-2022 Ian Stapleton Cordasco"
__version__ = "3.1.2"
__version__ = "3.2.0"
__version_info__ = tuple(
int(i) for i in __version__.split(".") if i.isdigit()
)
Expand Down
28 changes: 18 additions & 10 deletions src/github3/models.py
@@ -1,16 +1,24 @@
"""This module provides the basic models used in github3.py."""
import json as jsonlib
import logging
import typing as t

import dateutil.parser
import requests.compat

from . import exceptions
from . import session


if t.TYPE_CHECKING:
from . import structs

LOG = logging.getLogger(__package__)


T = t.TypeVar("T")


class GitHubCore:
"""The base object for all objects that require a session.
Expand All @@ -20,9 +28,9 @@ class GitHubCore:
"""

_ratelimit_resource = "core"
_refresh_to = None
_refresh_to: t.Optional["GitHubCore"] = None

def __init__(self, json, session):
def __init__(self, json, session: session.GitHubSession):
"""Initialize our basic object.
Pretty much every object will pass in decoded JSON and a Session.
Expand Down Expand Up @@ -244,14 +252,14 @@ def _api(self, uri):

def _iter(
self,
count,
url,
cls,
params=None,
etag=None,
headers=None,
list_key=None,
):
count: int,
url: str,
cls: t.Type[T],
params: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
etag: t.Optional[str] = None,
headers: t.Optional[t.Mapping[str, str]] = None,
list_key: t.Optional[str] = None,
) -> "structs.GitHubIterator[T]":
"""Generic iterator for this project.
:param int count: How many items to return.
Expand Down
46 changes: 34 additions & 12 deletions src/github3/orgs.py
Expand Up @@ -144,7 +144,7 @@ def edit(
bool
"""
if name:
data = {"name": name}
data: t.Dict[str, t.Union[str, int]] = {"name": name}
if permission:
data["permission"] = permission
if parent_team_id is not None:
Expand Down Expand Up @@ -478,7 +478,14 @@ def add_repository(self, repository, team_id): # FIXME(jlk): add perms
if int(team_id) < 0:
return False

url = self._build_url("teams", str(team_id), "repos", str(repository))
url = self._build_url(
"organizations",
str(self.id),
"team",
str(team_id),
"repos",
str(repository),
)
return self._boolean(self._put(url), 204, 404)

@requires_auth
Expand Down Expand Up @@ -736,10 +743,14 @@ def create_team(
:rtype:
:class:`~github3.orgs.Team`
"""
data = {
data: t.Dict[str, t.Union[t.List[str], str, int]] = {
"name": name,
"repo_names": [getattr(r, "full_name", r) for r in repo_names],
"maintainers": [getattr(m, "login", m) for m in maintainers],
"repo_names": [
getattr(r, "full_name", r) for r in (repo_names or [])
],
"maintainers": [
getattr(m, "login", m) for m in (maintainers or [])
],
"permission": permission,
"privacy": privacy,
}
Expand Down Expand Up @@ -1149,7 +1160,7 @@ def teams(self, number=-1, etag=None):
return self._iter(int(number), url, ShortTeam, etag=etag)

@requires_auth
def publicize_member(self, username):
def publicize_member(self, username: str) -> bool:
"""Make ``username``'s membership in this organization public.
:param str username:
Expand All @@ -1163,7 +1174,7 @@ def publicize_member(self, username):
return self._boolean(self._put(url), 204, 404)

@requires_auth
def remove_member(self, username):
def remove_member(self, username: str) -> bool:
"""Remove the user named ``username`` from this organization.
.. note::
Expand All @@ -1182,7 +1193,11 @@ def remove_member(self, username):
return self._boolean(self._delete(url), 204, 404)

@requires_auth
def remove_repository(self, repository, team_id):
def remove_repository(
self,
repository: t.Union[Repository, ShortRepository, str],
team_id: int,
):
"""Remove ``repository`` from the team with ``team_id``.
:param str repository:
Expand All @@ -1196,13 +1211,18 @@ def remove_repository(self, repository, team_id):
"""
if int(team_id) > 0:
url = self._build_url(
"teams", str(team_id), "repos", str(repository)
"organizations",
str(self.id),
"team",
str(team_id),
"repos",
str(repository),
)
return self._boolean(self._delete(url), 204, 404)
return False

@requires_auth
def team(self, team_id):
def team(self, team_id: int) -> t.Optional[Team]:
"""Return the team specified by ``team_id``.
:param int team_id:
Expand All @@ -1214,12 +1234,14 @@ def team(self, team_id):
"""
json = None
if int(team_id) > 0:
url = self._build_url("teams", str(team_id))
url = self._build_url(
"organizations", str(self.id), "team", str(team_id)
)
json = self._json(self._get(url), 200)
return self._instance_or_null(Team, json)

@requires_auth
def team_by_name(self, team_slug):
def team_by_name(self, team_slug: str) -> t.Optional[Team]:
"""Return the team specified by ``team_slug``.
:param str team_slug:
Expand Down
84 changes: 50 additions & 34 deletions src/github3/structs.py
@@ -1,66 +1,75 @@
import collections.abc as abc_collections
import collections.abc
import functools
import typing as t

from requests.compat import urlencode
from requests.compat import urlparse

from . import exceptions
from . import models

if t.TYPE_CHECKING:
import requests.models

class GitHubIterator(models.GitHubCore, abc_collections.Iterator):
from . import session


T = t.TypeVar("T")


class GitHubIterator(models.GitHubCore, collections.abc.Iterator):
"""The :class:`GitHubIterator` class powers all of the iter_* methods."""

def __init__(
self,
count,
url,
cls,
session,
params=None,
etag=None,
headers=None,
list_key=None,
):
count: int,
url: str,
cls: t.Type[T],
session: "session.GitHubSession",
params: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
etag: t.Optional[str] = None,
headers: t.Optional[t.Mapping[str, str]] = None,
list_key: t.Optional[str] = None,
) -> None:
models.GitHubCore.__init__(self, {}, session)
#: Original number of items requested
self.original = count
self.original: t.Final[int] = count
#: Number of items left in the iterator
self.count = count
self.count: int = count
#: URL the class used to make it's first GET
self.url = url
self.url: str = url
#: Last URL that was requested
self.last_url = None
self._api = self.url
self.last_url: t.Optional[str] = None
self._api: str = self.url
#: Class for constructing an item to return
self.cls = cls
self.cls: t.Type[T] = cls
#: Parameters of the query string
self.params = params or {}
self.params: t.Mapping[str, t.Optional[str]] = params or {}
self._remove_none(self.params)
# We do not set this from the parameter sent. We want this to
# represent the ETag header returned by GitHub no matter what.
# If this is not None, then it won't be set from the response and
# that's not what we want.
#: The ETag Header value returned by GitHub
self.etag = None
self.etag: t.Optional[str] = None
#: Headers generated for the GET request
self.headers = headers or {}
self.headers: t.Dict[str, str] = dict(headers or {})
#: The last response seen
self.last_response = None
self.last_response: "requests.models.Response" = None
#: Last status code received
self.last_status = 0
self.last_status: int = 0
#: Key to get the list of items in case a dict is returned
self.list_key = list_key
self.list_key: t.Final[t.Optional[str]] = list_key

if etag:
self.headers.update({"If-None-Match": etag})

self.path = urlparse(self.url).path
self.path: str = urlparse(self.url).path

def _repr(self):
def _repr(self) -> str:
return f"<GitHubIterator [{self.count}, {self.path}]>"

def __iter__(self):
def __iter__(self) -> t.Generator[T, None, None]:
self.last_url, params = self.url, self.params
headers = self.headers

Expand Down Expand Up @@ -127,23 +136,23 @@ def __iter__(self):
rel_next = response.links.get("next", {})
self.last_url = rel_next.get("url", "")

def __next__(self):
def __next__(self) -> T:
if not hasattr(self, "__i__"):
self.__i__ = self.__iter__()
return next(self.__i__)

def _get_json(self, response):
def _get_json(self, response: "requests.models.Response"):
return self._json(response, 200)

def refresh(self, conditional=False):
def refresh(self, conditional: bool = False) -> "GitHubIterator":
self.count = self.original
if conditional:
if conditional and self.etag:
self.headers["If-None-Match"] = self.etag
self.etag = None
self.__i__ = self.__iter__()
return self

def next(self):
def next(self) -> T:
return self.__next__()


Expand All @@ -160,13 +169,20 @@ class SearchIterator(GitHubIterator):
_ratelimit_resource = "search"

def __init__(
self, count, url, cls, session, params=None, etag=None, headers=None
self,
count: int,
url: str,
cls: t.Type[T],
session: "session.GitHubSession",
params: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
etag: t.Optional[str] = None,
headers: t.Optional[t.Mapping[str, str]] = None,
):
super().__init__(count, url, cls, session, params, etag, headers)
#: Total count returned by GitHub
self.total_count = 0
self.total_count: int = 0
#: Items array returned in the last request
self.items = []
self.items: t.List[t.Mapping[str, t.Any]] = []

def _repr(self):
return "<SearchIterator [{}, {}?{}]>".format(
Expand Down

0 comments on commit 53f9718

Please sign in to comment.