Skip to content

Commit bb54072

Browse files
committed
Refactor base to handler design
1 parent 81f483a commit bb54072

File tree

7 files changed

+116
-200
lines changed

7 files changed

+116
-200
lines changed

github3/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# -*- coding: utf-8 -*-
22

3-
from core import *
3+
#from core import *

github3/api.py

Lines changed: 58 additions & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -1,207 +1,68 @@
1-
# -*- coding: utf-8 -*-
2-
3-
"""
4-
github3.api
5-
~~~~~~~~~~~
6-
7-
This module provies the core GitHub3 API interface.
8-
"""
9-
10-
from urlparse import urlparse, parse_qs
1+
#!/usr/bin/env python
2+
# -*- encoding: utf-8 -*-
3+
#
4+
# author: David Medina
115

126
import requests
13-
from decorator import decorator
14-
15-
from .packages import omnijson as json
16-
from .packages.link_header import parse_link_value
17-
18-
from .helpers import is_collection, to_python, to_api, get_scope
19-
from .config import settings
20-
import handlers
7+
import json
8+
from errors import GithubError
9+
import github3.exceptions as ghexceptions
2110

22-
23-
PAGING_SIZE = 100
11+
RESOURCES_PER_PAGE = 100
2412

2513
class GithubCore(object):
14+
""" Wrapper for requests """
2615

27-
_rate_limit = None
28-
_rate_limit_remaining = None
16+
requests_remaining = None
2917

3018
def __init__(self):
3119
self.session = requests.session()
32-
self.session.params = {'per_page': PAGING_SIZE}
33-
34-
35-
@staticmethod
36-
def _resource_serialize(o):
37-
"""Returns JSON serialization of given object."""
38-
return json.dumps(o)
39-
40-
41-
@staticmethod
42-
def _resource_deserialize(s):
43-
"""Returns dict deserialization of a given JSON string."""
44-
20+
self.session.params = {'per_page': RESOURCES_PER_PAGE}
21+
self._parser = json
22+
23+
#@paginate to slice a generator after
24+
def get(self, request, **kwargs):
25+
response = self._request('GET', request, **kwargs)
26+
return self._parser.loads(response.content)
27+
28+
def head(self, request, **kwargs):
29+
return self._request('HEAD', request, **kwargs).headers
30+
31+
def post(self, request, data=None, **kwargs):
32+
kwargs['data'] = self._parser.dumps(data)
33+
response = self._request('POST', request, **kwargs)
34+
assert response.status_code == 201
35+
return self._parser.loads(response.content)
36+
37+
def patch(self, request, data=None, **kwargs):
38+
kwargs['data'] = self._parser.dumps(data)
39+
response = self._request('PATCH', request, **kwargs)
40+
assert response.status_code == 200
41+
return self._parser.loads(response.content)
42+
43+
def put(self, request, **kwargs):
44+
response = self._request('PUT', request, **kwargs)
45+
assert response.status_code == 204
46+
47+
def bool(self, request, **kwargs):
4548
try:
46-
return json.loads(s)
47-
except ValueError:
48-
raise ResponseError('The API Response was not valid.')
49-
50-
51-
@staticmethod
52-
def _generate_url(endpoint):
53-
"""Generates proper endpoint URL."""
54-
55-
if is_collection(endpoint):
56-
resource = map(str, endpoint)
57-
resource = '/'.join(resource)
58-
else:
59-
resource = endpoint
60-
61-
return (settings.base_url + resource)
62-
63-
64-
def _requests_post_hook(self, r):
65-
"""Post-processing for HTTP response objects."""
66-
67-
self._rate_limit = int(r.headers.get('x-ratelimit-limit', -1))
68-
self._rate_limit_remaining = int(r.headers.get('x-ratelimit-remaining', -1))
69-
70-
return r
71-
72-
73-
def _http_resource(self, verb, endpoint, params=None, check_status=True, **etc):
74-
75-
url = self._generate_url(endpoint)
76-
args = (verb, url)
77-
78-
if params:
79-
kwargs = {'params': params}
80-
kwargs.update(etc)
81-
else:
82-
kwargs = etc
83-
84-
r = self.session.request(*args, **kwargs)
85-
r = self._requests_post_hook(r)
86-
87-
if check_status:
88-
r.raise_for_status()
89-
90-
return r
91-
92-
93-
def _get_resource(self, resource, obj, **kwargs):
94-
95-
r = self._http_resource('GET', resource, params=kwargs)
96-
item = self._resource_deserialize(r.content)
97-
98-
return obj.new_from_dict(item, gh=self)
99-
100-
def _patch_resource(self, resource, data, **kwargs):
101-
r = self._http_resource('PATCH', resource, data=data, params=kwargs)
102-
msg = self._resource_deserialize(r.content)
103-
104-
return msg
105-
106-
107-
@staticmethod
108-
def _total_pages_from_header(link_header):
109-
110-
if link_header is None:
111-
return link_header
112-
113-
page_info = {}
114-
115-
for link in link_header.split(','):
116-
117-
uri, meta = map(str.strip, link.split(';'))
118-
119-
# Strip <>'s
120-
uri = uri[1:-1]
121-
122-
# Get query params from header.
123-
q = parse_qs(urlparse(uri).query)
124-
meta = meta[5:-1]
125-
126-
page_info[meta] = q
127-
128-
try:
129-
return int(page_info['last']['page'].pop())
130-
except KeyError:
131-
return True
132-
133-
def _get_resources(self, resource, obj, limit=None, **kwargs):
134-
135-
if limit is not None:
136-
assert limit > 0
137-
138-
moar = True
139-
is_truncated = (limit > PAGING_SIZE) or (limit is None)
140-
r_count = 0
141-
page = 1
142-
143-
while moar:
144-
145-
if not is_truncated:
146-
kwargs['per_page'] = limit
147-
moar = False
148-
else:
149-
kwargs['page'] = page
150-
if limit:
151-
if (limit - r_count) < PAGING_SIZE:
152-
kwargs['per_page'] = (limit - r_count)
153-
moar = False
154-
155-
r = self._http_resource('GET', resource, params=kwargs)
156-
max_page = self._total_pages_from_header(r.headers['link'])
157-
158-
if (max_page is True) or (max_page is None):
159-
moar = False
160-
161-
d_items = self._resource_deserialize(r.content)
162-
163-
for item in d_items:
164-
if (r_count < limit) or (limit is None):
165-
r_count += 1
166-
yield obj.new_from_dict(item, gh=self)
167-
else:
168-
moar = False
169-
170-
page += 1
171-
172-
def _get_bool(self, resource):
173-
resp = self._http_resource('GET', resource, check_status=False)
174-
return True if resp.status_code == 204 else False
175-
176-
def _get_raw(self, resource):
177-
resp = self._http_resource('GET', resource)
178-
return self._resource_deserialize(resp.content)
179-
180-
def _to_map(self, obj, iterable):
181-
"""Maps given dict iterable to a given Resource object."""
182-
183-
a = list()
184-
185-
for it in iterable:
186-
a.append(obj.new_from_dict(it, rdd=self))
187-
188-
return a
189-
190-
class Github(GithubCore):
191-
"""docstring for Github"""
192-
193-
def __init__(self):
194-
super(Github, self).__init__()
195-
self.is_authenticated = False
196-
197-
def user_handler(self, username=None, **kwargs):
198-
if kwargs.get('force') or not getattr(self, '_user_handler', False):
199-
if kwargs.get('private'):
200-
self._user_handler = handlers.AuthUser(self)
201-
else:
202-
self._user_handler = handlers.User(self, username)
203-
return self._user_handler
204-
205-
class ResponseError(Exception):
206-
"""The API Response was unexpected."""
207-
49+
response = self._request('GET', request, **kwargs)
50+
except ghexceptions.NotFound:
51+
return False
52+
assert response.status_code == 204
53+
return True
54+
55+
def delete(self, request, **kwargs):
56+
response = self._request('DELETE', request, **kwargs)
57+
assert response.status_code == 204
58+
59+
def _request(self, verb, request, **kwargs):
60+
61+
request = settings.base_url + request
62+
response = self.session.request(verb, request, **kwargs)
63+
self.requests_remaining = response.headers.get(
64+
'x-ratelimit-remaining',-1)
65+
error = GithubError(response)
66+
error.process()
67+
68+
return response

github3/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,5 @@ def __getattribute__(self, key):
5454
return object.__getattribute__(self, key)
5555

5656
settings = Settings()
57-
settings.verbose = False
58-
settings.base_url = 'https://api.github.com/'
57+
settings.verbose = True
58+
settings.base_url = 'https://api.github.com/'

github3/errors.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python
2+
# -*- encoding: utf-8 -*-
3+
#
4+
# author: David Medina
5+
6+
import json
7+
import github3.exceptions as exceptions
8+
9+
class GithubError(object):
10+
""" Handler for API errors """
11+
12+
def __init__(self, response):
13+
self._parser = json
14+
self.status_code = response.status_code
15+
self.debug = self._parser.loads(response.content)
16+
17+
def error_400(self):
18+
return exceptions.BadRequest("400 - %s" % self.debug.get('message'))
19+
20+
def error_404(self)
21+
return exceptions.NotFound("404 - %s" % self.debug.get('message'))
22+
23+
def error_422(self):
24+
errors = self.debug.get('errors')
25+
if errors:
26+
errors = ['{resource}: {code} => {field}'.format(**error)
27+
for error in errors]
28+
return exceptions.UnprocessableEntity(
29+
'422 - %s %s' % (self.debug.get('message'), errors))
30+
31+
def process(self):
32+
raise_error = getattr(self, 'error_%s' % self.status_code, False)
33+
if raise_error:
34+
raise raise_error()

github3/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
#
44
# author: David Medina
55

6+
class BadRequest(Exception):
7+
pass
8+
class UnprocessableEntity(Exception):
9+
pass
10+
class NotFound(Exception):
11+
pass
612
class AnomUser(Exception):
713
""" Exception for AnomUser handler """
814
pass

github3/handlers/base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,11 @@ def _get_resources(self, *args, **kwargs):
3131
map_model = kwargs.get('model', self._model)
3232
return self._gh._get_resources(url, map_model, **kwargs)
3333

34+
def _post_raw(self, *args, **kwargs):
35+
url = self._extend_url(*args)
36+
return self._gh._post_raw(url, **kwargs)
37+
38+
def _delete_raw(self, *args, **kwargs):
39+
url = self._extend_url(*args)
40+
return self._gh._delete_raw(url, **kwargs)
41+

github3/handlers/user.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,10 @@ def get_keys(self):
7272
def get_key(self, key_id):
7373
return self._get_resource('keys', key_id, model=models.Key)
7474

75+
def post_emails(self, *emails):
76+
emails_parsed = map(str, emails)
77+
return self._post_raw('emails', data=emails_parsed)
78+
79+
def delete_emails(self, *emails):
80+
emails_parsed = map(str, emails)
81+
return self._delete_raw('emails', data=emails_parsed)

0 commit comments

Comments
 (0)