Skip to content

Commit dd094a5

Browse files
authored
Merge pull request #18 from soda480/paging
Paging
2 parents 4c2c58e + 4331ed2 commit dd094a5

File tree

5 files changed

+6
-222
lines changed

5 files changed

+6
-222
lines changed

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# github3api #
22
[![GitHub Workflow Status](https://github.com/soda480/github3api/workflows/build/badge.svg)](https://github.com/soda480/github3api/actions)
33
[![Code Coverage](https://codecov.io/gh/soda480/github3api/branch/master/graph/badge.svg)](https://codecov.io/gh/soda480/github3api)
4-
[![Code Grade](https://www.code-inspector.com/project/13337/status/svg)](https://frontend.code-inspector.com/project/13337/dashboard)
4+
[![Code Grade](https://api.codiga.io/project/13337/status/svg)](https://frontend.code-inspector.com/project/13337/dashboard)
55
[![vulnerabilities](https://img.shields.io/badge/vulnerabilities-None-brightgreen)](https://pypi.org/project/bandit/)
6-
[![PyPI version](https://badge.fury.io/py/github3api.svg)](https://badge.fury.io/py/github3api)
6+
[![PyPI version](https://badge.fury.io/py/github3api.svg)](https://app.codiga.io/public/project/13337/github3api/dashboard)
77
[![python](https://img.shields.io/badge/python-3.9-teal)](https://www.python.org/downloads/)
88

99
An advanced REST client for the GitHub API. It is a subclass of [rest3client](https://pypi.org/project/rest3client/) tailored for the GitHub API with special optional directives for GET requests that can return all pages from an endpoint or return a generator that can be iterated over (for paged requests). By default all requests will be retried if ratelimit request limit is reached.
@@ -64,7 +64,7 @@ pip install github3api
6464

6565
`GET all` directive - Get all pages from an endpoint and return list containing only matching attributes
6666
```python
67-
for repo in client.get('/user/repos', _get='all', _attributes=['full_name']):
67+
for repo in client.get('/orgs/edgexfoundry/repos', _get='all', _attributes=['full_name']):
6868
print(repo['full_name'])
6969
```
7070

@@ -78,7 +78,6 @@ for page in client.get('/user/repos', _get='page'):
7878
`total` - Get total number of resources at given endpoint
7979
```python
8080
print(client.total('/user/repos'))
81-
6218
8281
```
8382

8483
`graphql` - execute graphql query

build.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
authors = [Author('Emilio Reyes', 'emilio.reyes@intel.com')]
3131
summary = 'An advanced REST client for the GitHub API'
3232
url = 'https://github.com/soda480/github3api'
33-
version = '0.2.0'
33+
version = '0.3.0'
3434
default_task = [
3535
'clean',
3636
'analyze',
@@ -51,7 +51,7 @@ def set_properties(project):
5151
project.set_property('flake8_include_scripts', True)
5252
project.set_property('flake8_include_test_sources', True)
5353
project.set_property('flake8_ignore', 'E501, W503, F401, E722, W605')
54-
project.build_depends_on_requirements('requirements-build.txt')
54+
project.build_depends_on('mock')
5555
project.depends_on_requirements('requirements.txt')
5656
project.set_property('distutils_readme_description', True)
5757
project.set_property('distutils_description_overwrite', True)

requirements-build.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/main/python/github3api/githubapi.py

Lines changed: 1 addition & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -70,63 +70,6 @@ def get_headers(self, **kwargs):
7070
headers['Accept'] = f'application/vnd.github.{self.version}+json'
7171
return headers
7272

73-
def _get_next_endpoint(self, url):
74-
""" return next endpoint
75-
"""
76-
if not url:
77-
logger.debug('link header is empty')
78-
return
79-
endpoint = self.get_endpoint_from_url(url)
80-
logger.debug(f'next endpoint is: {endpoint}')
81-
return endpoint
82-
83-
def _get_all(self, endpoint, **kwargs):
84-
""" return all pages from endpoint
85-
"""
86-
logger.debug(f'get items from: {endpoint}')
87-
items = []
88-
while True:
89-
url = None
90-
response = super(GitHubAPI, self).get(endpoint, raw_response=True, **kwargs)
91-
if response:
92-
data = response.json()
93-
if isinstance(data, list):
94-
items.extend(response.json())
95-
else:
96-
items.append(data)
97-
url = response.links.get('next', {}).get('url')
98-
99-
endpoint = self._get_next_endpoint(url)
100-
if not endpoint:
101-
logger.debug('no more pages to retrieve')
102-
break
103-
104-
return items
105-
106-
def _get_page(self, endpoint, **kwargs):
107-
""" return generator that yields pages from endpoint
108-
"""
109-
while True:
110-
response = super(GitHubAPI, self).get(endpoint, raw_response=True, **kwargs)
111-
yield response.json()
112-
endpoint = self._get_next_endpoint(response.links.get('next', {}).get('url'))
113-
if not endpoint:
114-
logger.debug('no more pages')
115-
break
116-
117-
def get(self, endpoint, **kwargs):
118-
""" ovverride get to provide paging support
119-
"""
120-
directive = kwargs.pop('_get', None)
121-
attributes = kwargs.pop('_attributes', None)
122-
if directive == 'all':
123-
items = self._get_all(endpoint, **kwargs)
124-
return GitHubAPI.match_keys(items, attributes)
125-
elif directive == 'page':
126-
return self._get_page(endpoint, **kwargs)
127-
else:
128-
return super(GitHubAPI, self).get(endpoint, **kwargs)
129-
13073
def total(self, endpoint):
13174
""" return total number of resources
13275
"""
@@ -140,7 +83,7 @@ def total(self, endpoint):
14083
response = self.get(endpoint, raw_response=True)
14184
if response.links:
14285
last_url = response.links['last']['url']
143-
endpoint = self.get_endpoint_from_url(last_url)
86+
endpoint = self._get_endpoint_from_url(last_url)
14487
items = self.get(endpoint)
14588
per_page = GitHubAPI.get_per_page_from_url(last_url)
14689
last_page = GitHubAPI.get_page_from_url(last_url)
@@ -150,11 +93,6 @@ def total(self, endpoint):
15093
total = len(items)
15194
return total
15295

153-
def get_endpoint_from_url(self, url):
154-
""" return endpoint from url
155-
"""
156-
return url.replace(f'https://{self.hostname}', '')
157-
15896
@staticmethod
15997
def get_page_from_url(url):
16098
""" get page query parameter form url
@@ -206,19 +144,6 @@ def log_ratelimit(ratelimit):
206144
"""
207145
logger.debug(f"{ratelimit['remaining']}/{ratelimit['limit']} resets in {ratelimit['minutes']} min")
208146

209-
@staticmethod
210-
def match_keys(items, attributes):
211-
""" return list of items with matching keys from list of attributes
212-
"""
213-
if not attributes:
214-
return items
215-
matched_items = []
216-
for item in items:
217-
matched_items.append({
218-
key: item[key] for key in attributes if key in item
219-
})
220-
return matched_items
221-
222147
@staticmethod
223148
def retry_ratelimit_error(exception):
224149
""" return True if exception is 403 HTTPError, False otherwise

src/unittest/python/test_githubapi.py

Lines changed: 0 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -73,29 +73,6 @@ def tearDown(self):
7373

7474
pass
7575

76-
def test__match_keys_Should_Return_Items_When_NoAttributes(self, *patches):
77-
result = GitHubAPI.match_keys(self.items, None)
78-
self.assertEqual(result, self.items)
79-
80-
def test__match_keys_Should_ReturnExpected_When_Called(self, *patches):
81-
result = GitHubAPI.match_keys(self.items, ['name', 'key1'])
82-
expected_result = [
83-
{
84-
'name': 'name1-mid-last1',
85-
'key1': 'value1'
86-
}, {
87-
'name': 'name2-mid-last2',
88-
'key1': 'value1'
89-
}, {
90-
'name': 'name3-med-last3',
91-
'key1': 'value1'
92-
}, {
93-
'name': 'name4-mid-last4',
94-
'key1': 'value1'
95-
}
96-
]
97-
self.assertEqual(result, expected_result)
98-
9976
def test__get_ratelimit_Should_ReturnExpected_When_NoHeader(self, *patches):
10077
result = GitHubAPI.get_ratelimit({})
10178
expected_result = {}
@@ -184,116 +161,6 @@ def test__get_next_endpoint_Should_ReturnExpected_When_CalledWithNextEndpoint(se
184161
expected_result = '/organizations/27781926/repos?page=4'
185162
self.assertEqual(result, expected_result)
186163

187-
@patch('github3api.GitHubAPI._get_next_endpoint')
188-
@patch('github3api.githubapi.RESTclient.get')
189-
def test__get_all_Should_ReturnExpected_When_GetReturnsList(self, get_patch, get_next_endpoint_patch, *patches):
190-
response_mock1 = Mock()
191-
response_mock1.json.return_value = ['item1', 'item2']
192-
response_mock2 = Mock()
193-
response_mock2.json.return_value = ['item3', 'item4']
194-
get_patch.side_effect = [
195-
response_mock1,
196-
response_mock2
197-
]
198-
get_next_endpoint_patch.side_effect = [
199-
{'Link': 'link-header-value'},
200-
{}
201-
]
202-
client = GitHubAPI(bearer_token='bearer-token')
203-
result = client._get_all('/repos/edgexfoundry/cd-management/milestones')
204-
expected_result = ['item1', 'item2', 'item3', 'item4']
205-
self.assertEqual(result, expected_result)
206-
207-
@patch('github3api.GitHubAPI._get_next_endpoint')
208-
@patch('github3api.githubapi.RESTclient.get')
209-
def test__get_all_Should_ReturnExpected_When_GetReturnsDict(self, get_patch, get_next_endpoint_patch, *patches):
210-
response_mock1 = Mock()
211-
response_mock1.json.return_value = {'key1': 'value1'}
212-
response_mock2 = Mock()
213-
response_mock2.json.return_value = {'key2': 'value2'}
214-
get_patch.side_effect = [
215-
response_mock1,
216-
response_mock2
217-
]
218-
get_next_endpoint_patch.side_effect = [
219-
{'Link': 'link-header-value'},
220-
{}
221-
]
222-
client = GitHubAPI(bearer_token='bearer-token')
223-
result = client._get_all('/repos/edgexfoundry/cd-management/milestones')
224-
expected_result = [{'key1': 'value1'}, {'key2': 'value2'}]
225-
self.assertEqual(result, expected_result)
226-
227-
@patch('github3api.GitHubAPI._get_next_endpoint')
228-
@patch('github3api.githubapi.RESTclient.get')
229-
def test__get_all_Should_ReturnEmptyList_When_NoResponse(self, get_patch, get_next_endpoint_patch, *patches):
230-
get_patch.side_effect = [
231-
None
232-
]
233-
get_next_endpoint_patch.side_effect = [
234-
None
235-
]
236-
client = GitHubAPI(bearer_token='bearer-token')
237-
result = client._get_all('/repos/edgexfoundry/cd-management/milestones')
238-
expected_result = []
239-
self.assertEqual(result, expected_result)
240-
241-
@patch('github3api.GitHubAPI._get_next_endpoint')
242-
@patch('github3api.githubapi.RESTclient.get')
243-
def test__get_page_Should_ReturnExpected_When_Called(self, get_patch, get_next_endpoint_patch, *patches):
244-
response_mock1 = Mock()
245-
response_mock1.json.return_value = ['page1', 'page2']
246-
response_mock2 = Mock()
247-
response_mock2.json.return_value = ['page3', 'page4']
248-
get_patch.side_effect = [response_mock1, response_mock2]
249-
get_next_endpoint_patch.return_value = ['next-endpoint', 'next-endpoint', None]
250-
client = GitHubAPI(bearer_token='bearer-token')
251-
result = client._get_page('endpoint')
252-
self.assertEqual(next(result), ['page1', 'page2'])
253-
self.assertEqual(next(result), ['page3', 'page4'])
254-
# with self.assertRaises(StopIteration):
255-
# next(result)
256-
257-
@patch('github3api.GitHubAPI._get_next_endpoint')
258-
@patch('github3api.githubapi.RESTclient.get')
259-
def test__get_page_Should_ReturnExpected_When_NoEndpoint(self, get_patch, get_next_endpoint_patch, *patches):
260-
response_mock1 = Mock()
261-
response_mock1.json.return_value = ['page1', 'page2']
262-
get_patch.side_effect = [response_mock1]
263-
get_next_endpoint_patch.side_effect = [None]
264-
client = GitHubAPI(bearer_token='bearer-token')
265-
result = client._get_page('endpoint')
266-
self.assertEqual(next(result), ['page1', 'page2'])
267-
with self.assertRaises(StopIteration):
268-
next(result)
269-
270-
@patch('github3api.GitHubAPI.match_keys')
271-
@patch('github3api.GitHubAPI._get_all')
272-
def test__get_Should_CallExpected_When_AllDirective(self, get_all_patch, match_keys_patch, *patches):
273-
client = GitHubAPI(bearer_token='bearer-token')
274-
endpoint = '/repos/edgexfoundry/cd-management/milestones'
275-
attributes = ['key1', 'key2']
276-
result = client.get(endpoint, _get='all', _attributes=attributes)
277-
get_all_patch.assert_called_once_with(endpoint)
278-
match_keys_patch.assert_called_once_with(get_all_patch.return_value, attributes)
279-
self.assertEqual(result, match_keys_patch.return_value)
280-
281-
@patch('github3api.GitHubAPI._get_page')
282-
def test__get_Should_CallExpected_When_GenDirective(self, get_page_patch, *patches):
283-
client = GitHubAPI(bearer_token='bearer-token')
284-
endpoint = '/repos/edgexfoundry/cd-management/milestones'
285-
result = client.get(endpoint, _get='page')
286-
get_page_patch.assert_called_once_with(endpoint)
287-
self.assertEqual(result, get_page_patch.return_value)
288-
289-
@patch('github3api.githubapi.RESTclient.get')
290-
def test__get_Should_CallExpected_When_NoDirective(self, get_patch, *patches):
291-
client = GitHubAPI(bearer_token='bearer-token')
292-
endpoint = '/repos/edgexfoundry/cd-management/milestones'
293-
result = client.get(endpoint, k1='v1', k2='v2')
294-
get_patch.assert_called_once_with(endpoint, k1='v1', k2='v2')
295-
self.assertEqual(result, get_patch.return_value)
296-
297164
@patch('github3api.githubapi.getenv', return_value='token')
298165
@patch('github3api.githubapi.GitHubAPI')
299166
def test__get_client_Should_CallAndReturnExpected_When_Called(self, githubapi_patch, getenv_patch, *patches):
@@ -322,12 +189,6 @@ def test__get_retries_Should_ReturnExpected_When_Called(self, *patches):
322189
]
323190
self.assertEqual(client.retries, expected_retries)
324191

325-
def test__get_endpoint_from_url_Should_ReturnExpected_When_Called(self, *patches):
326-
client = GitHubAPI(bearer_token='bearer-token')
327-
result = client.get_endpoint_from_url('https://api.github.com/user/repos?page=2')
328-
expected_result = '/user/repos?page=2'
329-
self.assertEqual(result, expected_result)
330-
331192
def test__get_page_from_url_Should_ReturnExpected_When_Match(self, *patches):
332193
result = GitHubAPI.get_page_from_url('https://api.github.com/user/repos?page=213')
333194
expected_result = 213

0 commit comments

Comments
 (0)