1
1
# Copyright 2012-2016 Jonathan Paugh and contributors
2
2
# See COPYING for license details
3
3
import base64
4
+ import time
5
+ import re
4
6
5
- from agithub .base import API , ConnectionProperties , Client
7
+ from agithub .base import API , ConnectionProperties , Client , RequestBody , ResponseBody
6
8
7
9
8
10
class GitHub (API ):
9
11
"""
10
12
The agnostic GitHub API. It doesn't know, and you don't care.
11
- >>> from agithub import GitHub
13
+ >>> from agithub.GitHub import GitHub
12
14
>>> g = GitHub('user', 'pass')
13
15
>>> status, data = g.issues.get(filter='subscribed')
14
16
>>> data
@@ -33,7 +35,7 @@ class GitHub(API):
33
35
it automatically supports the full API--so why should you care?
34
36
"""
35
37
def __init__ (self , username = None , password = None , token = None ,
36
- * args , ** kwargs ):
38
+ paginate = False , * args , ** kwargs ):
37
39
extraHeaders = {'accept' : 'application/vnd.github.v3+json' }
38
40
auth = self .generateAuthHeader (username , password , token )
39
41
if auth is not None :
@@ -44,7 +46,7 @@ def __init__(self, username=None, password=None, token=None,
44
46
extra_headers = extraHeaders
45
47
)
46
48
47
- self .setClient (Client ( * args , ** kwargs ))
49
+ self .setClient (GitHubClient ( paginate = paginate , * args , ** kwargs ))
48
50
self .setConnectionProperties (props )
49
51
50
52
def generateAuthHeader (self , username = None , password = None , token = None ):
@@ -66,3 +68,88 @@ def generateAuthHeader(self, username=None, password=None, token=None):
66
68
def hash_pass (self , password ):
67
69
auth_str = ('%s:%s' % (self .username , password )).encode ('utf-8' )
68
70
return 'Basic ' .encode ('utf-8' ) + base64 .b64encode (auth_str ).strip ()
71
+
72
+ class GitHubClient (Client ):
73
+ def __init__ (self , username = None , password = None , token = None ,
74
+ connection_properties = None , paginate = False ):
75
+ super (GitHubClient , self ).__init__ ()
76
+ self .paginate = paginate
77
+
78
+ def request (self , method , url , bodyData , headers ):
79
+ '''Low-level networking. All HTTP-method methods call this'''
80
+
81
+ headers = self ._fix_headers (headers )
82
+ url = self .prop .constructUrl (url )
83
+
84
+ if bodyData is None :
85
+ # Sending a content-type w/o the body might break some
86
+ # servers. Maybe?
87
+ if 'content-type' in headers :
88
+ del headers ['content-type' ]
89
+
90
+ #TODO: Context manager
91
+ requestBody = RequestBody (bodyData , headers )
92
+
93
+ if self .no_ratelimit_remaining ():
94
+ time .sleep (self .ratelimit_seconds_remaining ())
95
+
96
+ while True :
97
+ conn = self .get_connection ()
98
+ conn .request (method , url , requestBody .process (), headers )
99
+ response = conn .getresponse ()
100
+ status = response .status
101
+ content = ResponseBody (response )
102
+ self .headers = response .getheaders ()
103
+
104
+ conn .close ()
105
+ if status == '403' and self .no_ratelimit_remaining ():
106
+ time .sleep (self .ratelimit_seconds_remaining ())
107
+ else :
108
+ data = content .processBody ()
109
+ if self .paginate and type (data ) == list :
110
+ data .extend (self .get_additional_pages ())
111
+ return status , data
112
+
113
+ def get_additional_pages (self ):
114
+ data = []
115
+ url = self .get_next_link_url ()
116
+ if url :
117
+ status , data = self .get (url )
118
+ data .extend (self .get_additional_pages ())
119
+ return data
120
+
121
+ def no_ratelimit_remaining (self ):
122
+ headers = dict (self .headers if self .headers is not None else [])
123
+ return int (headers .get ('X-RateLimit-Remaining' , 1 )) == 0
124
+
125
+ def ratelimit_seconds_remaining (self ):
126
+ ratelimit_reset = int (dict (self .headers ).get (
127
+ 'X-RateLimit-Reset' , 0 ))
128
+ return max (0 , ratelimit_reset - time .time ())
129
+
130
+ def get_next_link_url (self ):
131
+ '''Given a set of HTTP headers find the RFC 5988 Link header field,
132
+ determine if it contains a relation type indicating a next resource and if
133
+ so return the URL of the next resource, otherwise return an empty string.
134
+ '''
135
+ # From https://github.com/requests/requests/blob/master/requests/utils.py
136
+ for value in [x [1 ] for x in self .headers if x [0 ].lower () == 'link' ]:
137
+ replace_chars = ' \' "'
138
+ value = value .strip (replace_chars )
139
+ if not value :
140
+ return ''
141
+ for val in re .split (', *<' , value ):
142
+ try :
143
+ url , params = val .split (';' , 1 )
144
+ except ValueError :
145
+ url , params = val , ''
146
+ link = {'url' : url .strip ('<> \' "' )}
147
+ for param in params .split (';' ):
148
+ try :
149
+ key , value = param .split ('=' )
150
+ except ValueError :
151
+ break
152
+ link [key .strip (replace_chars )] = value .strip (replace_chars )
153
+ if link .get ('rel' ) == 'next' :
154
+ return link ['url' ]
155
+ return ''
0 commit comments