-
Notifications
You must be signed in to change notification settings - Fork 406
/
Copy pathstructs.py
201 lines (167 loc) · 6.98 KB
/
structs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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
from typing_extensions import Final
from . import session
class GitHubIterator(models.GitHubCore, collections.abc.Iterator):
"""The :class:`GitHubIterator` class powers all of the iter_* methods."""
def __init__(
self,
count: int,
url: str,
cls: t.Type[models.GitHubCore],
session: "session.GitHubSession",
params: t.Optional[
t.MutableMapping[str, t.Union[str, int, None]]
] = 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: Final[int] = count
#: Number of items left in the iterator
self.count: int = count
#: URL the class used to make it's first GET
self.url: str = url
#: Last URL that was requested
self.last_url: t.Optional[str] = None
self._api: str = self.url
#: Class for constructing an item to return
self.cls: t.Type[models.GitHubCore] = cls
#: Parameters of the query string
self.params: t.MutableMapping[str, t.Optional[t.Union[str, int]]] = (
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: t.Optional[str] = None
#: Headers generated for the GET request
self.headers: t.Dict[str, str] = dict(headers or {})
#: The last response seen
self.last_response: t.Optional["requests.models.Response"] = None
#: Last status code received
self.last_status: int = 0
#: Key to get the list of items in case a dict is returned
self.list_key: Final[t.Optional[str]] = list_key
if etag:
self.headers.update({"If-None-Match": etag})
self.path: str = urlparse(self.url).path
def _repr(self) -> str:
return f"<GitHubIterator [{self.count}, {self.path}]>"
def __iter__(self) -> t.Iterator[models.GitHubCore]:
self.last_url, params = self.url, self.params
headers = self.headers
if 0 < self.count <= 100 and self.count != -1:
params["per_page"] = self.count
if "per_page" not in params and self.count == -1:
params["per_page"] = 100
cls = functools.partial(self.cls, session=self.session)
while (self.count == -1 or self.count > 0) and self.last_url:
response = self._get(
self.last_url, params=params, headers=headers
)
self.last_response = response
self.last_status = response.status_code
if params:
params = {} # rel_next already has the params
if not self.etag and response.headers.get("ETag"):
self.etag = response.headers.get("ETag")
json = self._get_json(response)
if json is None:
break
# Some APIs return the list of items inside a dict
if isinstance(json, dict) and self.list_key is not None:
try:
json = json[self.list_key]
except KeyError:
raise exceptions.UnprocessableResponseBody(
"GitHub's API returned a body that could not be"
" handled",
json,
)
# languages returns a single dict. We want the items.
if isinstance(json, dict):
if issubclass(self.cls, models.GitHubCore):
raise exceptions.UnprocessableResponseBody(
"GitHub's API returned a body that could not be"
" handled",
json,
)
if json.get("ETag"):
del json["ETag"]
if json.get("Last-Modified"):
del json["Last-Modified"]
json = json.items()
for i in json:
if i is None:
continue
yield cls(i)
self.count -= 1 if self.count > 0 else 0
if self.count == 0:
break
rel_next = response.links.get("next", {})
self.last_url = rel_next.get("url", "")
def __next__(self) -> models.GitHubCore:
if not hasattr(self, "__i__"):
self.__i__ = self.__iter__()
return next(self.__i__)
def _get_json(self, response: "requests.models.Response"):
return self._json(response, 200)
def refresh(self, conditional: bool = False) -> "GitHubIterator":
self.count = self.original
if conditional and self.etag:
self.headers["If-None-Match"] = self.etag
self.etag = None
self.__i__ = self.__iter__()
return self
def next(self) -> models.GitHubCore:
return self.__next__()
class SearchIterator(GitHubIterator):
"""This is a special-cased class for returning iterable search results.
It inherits from :class:`GitHubIterator <github3.structs.GitHubIterator>`.
All members and methods documented here are unique to instances of this
class. For other members and methods, check its parent class.
"""
_ratelimit_resource = "search"
def __init__(
self,
count: int,
url: str,
cls: t.Type[models.GitHubCore],
session: "session.GitHubSession",
params: t.Optional[
t.MutableMapping[str, t.Union[int, str, None]]
] = 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: int = 0
#: Items array returned in the last request
self.items: t.List[t.Mapping[str, t.Any]] = []
def _repr(self):
return "<SearchIterator [{}, {}?{}]>".format(
self.count, self.path, urlencode(self.params)
)
def _get_json(self, response):
json = self._json(response, 200)
# I'm not sure if another page will retain the total_count attribute,
# so if it's not in the response, just set it back to what it used to
# be
self.total_count = json.get("total_count", self.total_count)
self.items = json.get("items", [])
# If we return None then it will short-circuit the while loop.
return json.get("items")