We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
tools/github_stats fails on 2fa. 
```
$ python tools/github_stats.py --milestone 4.1 --since-tag 4.0.0
no cache
fetching GitHub stats since 2015-08-12 00:00:00 (tag: 4.0.0, milestone: 4.1)
Please enter your github username and password. These are not stored, only used to get an oAuth token. You can revoke this at any time on Github.
Username: *****
Password:*****
Traceback (most recent call last):
  File "github_stats.py", line 165, in <module>
    auth=True)
  File "/Users/bussonniermatthias/dev/ipython/tools/gh_api.py", line 175, in get_milestone_id
    milestones = get_milestones(project, auth=auth, **params)
  File "/Users/bussonniermatthias/dev/ipython/tools/gh_api.py", line 168, in get_milestones
    headers = make_auth_header()
  File "/Users/bussonniermatthias/dev/ipython/tools/gh_api.py", line 73, in make_auth_header
    return {'Authorization': 'token ' + get_auth_token()}
  File "/Users/bussonniermatthias/dev/ipython/tools/gh_api.py", line 67, in get_auth_token
    response.raise_for_status()
  File "/Users/bussonniermatthias/anaconda3/lib/python3.5/site-packages/requests/models.py", line 840, in raise_for_status
    raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.github.com/authorizations
```


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `tools/gh_api.py`
Content:
```
1 """Functions for Github API requests."""
2 from __future__ import print_function
3 
4 try:
5     input = raw_input
6 except NameError:
7     pass
8 
9 import os
10 import re
11 import sys
12 
13 import requests
14 import getpass
15 import json
16 
17 try:
18     import requests_cache
19 except ImportError:
20     print("no cache", file=sys.stderr)
21 else:
22     requests_cache.install_cache("gh_api", expire_after=3600)
23 
24 # Keyring stores passwords by a 'username', but we're not storing a username and
25 # password
26 fake_username = 'ipython_tools'
27 
28 class Obj(dict):
29     """Dictionary with attribute access to names."""
30     def __getattr__(self, name):
31         try:
32             return self[name]
33         except KeyError:
34             raise AttributeError(name)
35 
36     def __setattr__(self, name, val):
37         self[name] = val
38 
39 token = None
40 def get_auth_token():
41     global token
42 
43     if token is not None:
44         return token
45 
46     import keyring
47     token = keyring.get_password('github', fake_username)
48     if token is not None:
49         return token
50 
51     print("Please enter your github username and password. These are not "
52            "stored, only used to get an oAuth token. You can revoke this at "
53            "any time on Github.")
54     user = input("Username: ")
55     pw = getpass.getpass("Password: ")
56 
57     auth_request = {
58       "scopes": [
59         "public_repo",
60         "gist"
61       ],
62       "note": "IPython tools",
63       "note_url": "https://github.com/ipython/ipython/tree/master/tools",
64     }
65     response = requests.post('https://api.github.com/authorizations',
66                             auth=(user, pw), data=json.dumps(auth_request))
67     response.raise_for_status()
68     token = json.loads(response.text)['token']
69     keyring.set_password('github', fake_username, token)
70     return token
71 
72 def make_auth_header():
73     return {'Authorization': 'token ' + get_auth_token()}
74 
75 def post_issue_comment(project, num, body):
76     url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)
77     payload = json.dumps({'body': body})
78     requests.post(url, data=payload, headers=make_auth_header())
79 
80 def post_gist(content, description='', filename='file', auth=False):
81     """Post some text to a Gist, and return the URL."""
82     post_data = json.dumps({
83       "description": description,
84       "public": True,
85       "files": {
86         filename: {
87           "content": content
88         }
89       }
90     }).encode('utf-8')
91 
92     headers = make_auth_header() if auth else {}
93     response = requests.post("https://api.github.com/gists", data=post_data, headers=headers)
94     response.raise_for_status()
95     response_data = json.loads(response.text)
96     return response_data['html_url']
97 
98 def get_pull_request(project, num, auth=False):
99     """get pull request info  by number
100     """
101     url = "https://api.github.com/repos/{project}/pulls/{num}".format(project=project, num=num)
102     if auth:
103         header = make_auth_header()
104     else:
105         header = None
106     print("fetching %s" % url, file=sys.stderr)
107     response = requests.get(url, headers=header)
108     response.raise_for_status()
109     return json.loads(response.text, object_hook=Obj)
110 
111 def get_pull_request_files(project, num, auth=False):
112     """get list of files in a pull request"""
113     url = "https://api.github.com/repos/{project}/pulls/{num}/files".format(project=project, num=num)
114     if auth:
115         header = make_auth_header()
116     else:
117         header = None
118     return get_paged_request(url, headers=header)
119 
120 element_pat = re.compile(r'<(.+?)>')
121 rel_pat = re.compile(r'rel=[\'"](\w+)[\'"]')
122 
123 def get_paged_request(url, headers=None, **params):
124     """get a full list, handling APIv3's paging"""
125     results = []
126     params.setdefault("per_page", 100)
127     while True:
128         if '?' in url:
129             params = None
130             print("fetching %s" % url, file=sys.stderr)
131         else:
132             print("fetching %s with %s" % (url, params), file=sys.stderr)
133         response = requests.get(url, headers=headers, params=params)
134         response.raise_for_status()
135         results.extend(response.json())
136         if 'next' in response.links:
137             url = response.links['next']['url']
138         else:
139             break
140     return results
141 
142 def get_pulls_list(project, auth=False, **params):
143     """get pull request list"""
144     params.setdefault("state", "closed")
145     url = "https://api.github.com/repos/{project}/pulls".format(project=project)
146     if auth:
147         headers = make_auth_header()
148     else:
149         headers = None
150     pages = get_paged_request(url, headers=headers, **params)
151     return pages
152 
153 def get_issues_list(project, auth=False, **params):
154     """get issues list"""
155     params.setdefault("state", "closed")
156     url = "https://api.github.com/repos/{project}/issues".format(project=project)
157     if auth:
158         headers = make_auth_header()
159     else:
160         headers = None
161     pages = get_paged_request(url, headers=headers, **params)
162     return pages
163 
164 def get_milestones(project, auth=False, **params):
165     params.setdefault('state', 'all')
166     url = "https://api.github.com/repos/{project}/milestones".format(project=project)
167     if auth:
168         headers = make_auth_header()
169     else:
170         headers = None
171     milestones = get_paged_request(url, headers=headers, **params)
172     return milestones
173 
174 def get_milestone_id(project, milestone, auth=False, **params):
175     milestones = get_milestones(project, auth=auth, **params)
176     for mstone in milestones:
177         if mstone['title'] == milestone:
178             return mstone['number']
179     else:
180         raise ValueError("milestone %s not found" % milestone)
181 
182 def is_pull_request(issue):
183     """Return True if the given issue is a pull request."""
184     return bool(issue.get('pull_request', {}).get('html_url', None))
185 
186 def get_authors(pr):
187     print("getting authors for #%i" % pr['number'], file=sys.stderr)
188     h = make_auth_header()
189     r = requests.get(pr['commits_url'], headers=h)
190     r.raise_for_status()
191     commits = r.json()
192     authors = []
193     for commit in commits:
194         author = commit['commit']['author']
195         authors.append("%s <%s>" % (author['name'], author['email']))
196     return authors
197 
198 # encode_multipart_formdata is from urllib3.filepost
199 # The only change is to iter_fields, to enforce S3's required key ordering
200 
201 def iter_fields(fields):
202     fields = fields.copy()
203     for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',
204         'Policy', 'Signature', 'Content-Type', 'file'):
205         yield (key, fields.pop(key))
206     for (k,v) in fields.items():
207         yield k,v
208 
209 def encode_multipart_formdata(fields, boundary=None):
210     """
211     Encode a dictionary of ``fields`` using the multipart/form-data mime format.
212 
213     :param fields:
214         Dictionary of fields or list of (key, value) field tuples.  The key is
215         treated as the field name, and the value as the body of the form-data
216         bytes. If the value is a tuple of two elements, then the first element
217         is treated as the filename of the form-data section.
218 
219         Field names and filenames must be unicode.
220 
221     :param boundary:
222         If not specified, then a random boundary will be generated using
223         :func:`mimetools.choose_boundary`.
224     """
225     # copy requests imports in here:
226     from io import BytesIO
227     from requests.packages.urllib3.filepost import (
228         choose_boundary, six, writer, b, get_content_type
229     )
230     body = BytesIO()
231     if boundary is None:
232         boundary = choose_boundary()
233 
234     for fieldname, value in iter_fields(fields):
235         body.write(b('--%s\r\n' % (boundary)))
236 
237         if isinstance(value, tuple):
238             filename, data = value
239             writer(body).write('Content-Disposition: form-data; name="%s"; '
240                                'filename="%s"\r\n' % (fieldname, filename))
241             body.write(b('Content-Type: %s\r\n\r\n' %
242                        (get_content_type(filename))))
243         else:
244             data = value
245             writer(body).write('Content-Disposition: form-data; name="%s"\r\n'
246                                % (fieldname))
247             body.write(b'Content-Type: text/plain\r\n\r\n')
248 
249         if isinstance(data, int):
250             data = str(data)  # Backwards compatibility
251         if isinstance(data, six.text_type):
252             writer(body).write(data)
253         else:
254             body.write(data)
255 
256         body.write(b'\r\n')
257 
258     body.write(b('--%s--\r\n' % (boundary)))
259 
260     content_type = b('multipart/form-data; boundary=%s' % boundary)
261 
262     return body.getvalue(), content_type
263 
264 
265 def post_download(project, filename, name=None, description=""):
266     """Upload a file to the GitHub downloads area"""
267     if name is None:
268         name = os.path.basename(filename)
269     with open(filename, 'rb') as f:
270         filedata = f.read()
271 
272     url = "https://api.github.com/repos/{project}/downloads".format(project=project)
273 
274     payload = json.dumps(dict(name=name, size=len(filedata),
275                     description=description))
276     response = requests.post(url, data=payload, headers=make_auth_header())
277     response.raise_for_status()
278     reply = json.loads(response.content)
279     s3_url = reply['s3_url']
280 
281     fields = dict(
282         key=reply['path'],
283         acl=reply['acl'],
284         success_action_status=201,
285         Filename=reply['name'],
286         AWSAccessKeyId=reply['accesskeyid'],
287         Policy=reply['policy'],
288         Signature=reply['signature'],
289         file=(reply['name'], filedata),
290     )
291     fields['Content-Type'] = reply['mime_type']
292     data, content_type = encode_multipart_formdata(fields)
293     s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})
294     return s3r
295
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/tools/gh_api.py b/tools/gh_api.py
--- a/tools/gh_api.py
+++ b/tools/gh_api.py
@@ -64,6 +64,13 @@
     }
     response = requests.post('https://api.github.com/authorizations',
                             auth=(user, pw), data=json.dumps(auth_request))
+    if response.status_code == 401 and response.headers.get('X-GitHub-OTP') == 'required; sms':
+        print("Your login API resquest a SMS one time password")
+        sms_pw = getpass.getpass("SMS password: ")
+        response = requests.post('https://api.github.com/authorizations',
+                            auth=(user, pw), 
+                            data=json.dumps(auth_request),
+                            headers={'X-GitHub-OTP':sms_pw})
     response.raise_for_status()
     token = json.loads(response.text)['token']
     keyring.set_password('github', fake_username, token)

{"golden_diff": "diff --git a/tools/gh_api.py b/tools/gh_api.py\n--- a/tools/gh_api.py\n+++ b/tools/gh_api.py\n@@ -64,6 +64,13 @@\n     }\n     response = requests.post('https://api.github.com/authorizations',\n                             auth=(user, pw), data=json.dumps(auth_request))\n+    if response.status_code == 401 and response.headers.get('X-GitHub-OTP') == 'required; sms':\n+        print(\"Your login API resquest a SMS one time password\")\n+        sms_pw = getpass.getpass(\"SMS password: \")\n+        response = requests.post('https://api.github.com/authorizations',\n+                            auth=(user, pw), \n+                            data=json.dumps(auth_request),\n+                            headers={'X-GitHub-OTP':sms_pw})\n     response.raise_for_status()\n     token = json.loads(response.text)['token']\n     keyring.set_password('github', fake_username, token)\n", "issue": "tools/github_stats fails on 2fa. \n```\n$ python tools/github_stats.py --milestone 4.1 --since-tag 4.0.0\nno cache\nfetching GitHub stats since 2015-08-12 00:00:00 (tag: 4.0.0, milestone: 4.1)\nPlease enter your github username and password. These are not stored, only used to get an oAuth token. You can revoke this at any time on Github.\nUsername: *****\nPassword:*****\nTraceback (most recent call last):\n  File \"github_stats.py\", line 165, in <module>\n    auth=True)\n  File \"/Users/bussonniermatthias/dev/ipython/tools/gh_api.py\", line 175, in get_milestone_id\n    milestones = get_milestones(project, auth=auth, **params)\n  File \"/Users/bussonniermatthias/dev/ipython/tools/gh_api.py\", line 168, in get_milestones\n    headers = make_auth_header()\n  File \"/Users/bussonniermatthias/dev/ipython/tools/gh_api.py\", line 73, in make_auth_header\n    return {'Authorization': 'token ' + get_auth_token()}\n  File \"/Users/bussonniermatthias/dev/ipython/tools/gh_api.py\", line 67, in get_auth_token\n    response.raise_for_status()\n  File \"/Users/bussonniermatthias/anaconda3/lib/python3.5/site-packages/requests/models.py\", line 840, in raise_for_status\n    raise HTTPError(http_error_msg, response=self)\nrequests.exceptions.HTTPError: 401 Client Error: Unauthorized for url: https://api.github.com/authorizations\n```\n\n", "before_files": [{"content": "\"\"\"Functions for Github API requests.\"\"\"\nfrom __future__ import print_function\n\ntry:\n    input = raw_input\nexcept NameError:\n    pass\n\nimport os\nimport re\nimport sys\n\nimport requests\nimport getpass\nimport json\n\ntry:\n    import requests_cache\nexcept ImportError:\n    print(\"no cache\", file=sys.stderr)\nelse:\n    requests_cache.install_cache(\"gh_api\", expire_after=3600)\n\n# Keyring stores passwords by a 'username', but we're not storing a username and\n# password\nfake_username = 'ipython_tools'\n\nclass Obj(dict):\n    \"\"\"Dictionary with attribute access to names.\"\"\"\n    def __getattr__(self, name):\n        try:\n            return self[name]\n        except KeyError:\n            raise AttributeError(name)\n\n    def __setattr__(self, name, val):\n        self[name] = val\n\ntoken = None\ndef get_auth_token():\n    global token\n\n    if token is not None:\n        return token\n\n    import keyring\n    token = keyring.get_password('github', fake_username)\n    if token is not None:\n        return token\n\n    print(\"Please enter your github username and password. These are not \"\n           \"stored, only used to get an oAuth token. You can revoke this at \"\n           \"any time on Github.\")\n    user = input(\"Username: \")\n    pw = getpass.getpass(\"Password: \")\n\n    auth_request = {\n      \"scopes\": [\n        \"public_repo\",\n        \"gist\"\n      ],\n      \"note\": \"IPython tools\",\n      \"note_url\": \"https://github.com/ipython/ipython/tree/master/tools\",\n    }\n    response = requests.post('https://api.github.com/authorizations',\n                            auth=(user, pw), data=json.dumps(auth_request))\n    response.raise_for_status()\n    token = json.loads(response.text)['token']\n    keyring.set_password('github', fake_username, token)\n    return token\n\ndef make_auth_header():\n    return {'Authorization': 'token ' + get_auth_token()}\n\ndef post_issue_comment(project, num, body):\n    url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)\n    payload = json.dumps({'body': body})\n    requests.post(url, data=payload, headers=make_auth_header())\n\ndef post_gist(content, description='', filename='file', auth=False):\n    \"\"\"Post some text to a Gist, and return the URL.\"\"\"\n    post_data = json.dumps({\n      \"description\": description,\n      \"public\": True,\n      \"files\": {\n        filename: {\n          \"content\": content\n        }\n      }\n    }).encode('utf-8')\n\n    headers = make_auth_header() if auth else {}\n    response = requests.post(\"https://api.github.com/gists\", data=post_data, headers=headers)\n    response.raise_for_status()\n    response_data = json.loads(response.text)\n    return response_data['html_url']\n\ndef get_pull_request(project, num, auth=False):\n    \"\"\"get pull request info  by number\n    \"\"\"\n    url = \"https://api.github.com/repos/{project}/pulls/{num}\".format(project=project, num=num)\n    if auth:\n        header = make_auth_header()\n    else:\n        header = None\n    print(\"fetching %s\" % url, file=sys.stderr)\n    response = requests.get(url, headers=header)\n    response.raise_for_status()\n    return json.loads(response.text, object_hook=Obj)\n\ndef get_pull_request_files(project, num, auth=False):\n    \"\"\"get list of files in a pull request\"\"\"\n    url = \"https://api.github.com/repos/{project}/pulls/{num}/files\".format(project=project, num=num)\n    if auth:\n        header = make_auth_header()\n    else:\n        header = None\n    return get_paged_request(url, headers=header)\n\nelement_pat = re.compile(r'<(.+?)>')\nrel_pat = re.compile(r'rel=[\\'\"](\\w+)[\\'\"]')\n\ndef get_paged_request(url, headers=None, **params):\n    \"\"\"get a full list, handling APIv3's paging\"\"\"\n    results = []\n    params.setdefault(\"per_page\", 100)\n    while True:\n        if '?' in url:\n            params = None\n            print(\"fetching %s\" % url, file=sys.stderr)\n        else:\n            print(\"fetching %s with %s\" % (url, params), file=sys.stderr)\n        response = requests.get(url, headers=headers, params=params)\n        response.raise_for_status()\n        results.extend(response.json())\n        if 'next' in response.links:\n            url = response.links['next']['url']\n        else:\n            break\n    return results\n\ndef get_pulls_list(project, auth=False, **params):\n    \"\"\"get pull request list\"\"\"\n    params.setdefault(\"state\", \"closed\")\n    url = \"https://api.github.com/repos/{project}/pulls\".format(project=project)\n    if auth:\n        headers = make_auth_header()\n    else:\n        headers = None\n    pages = get_paged_request(url, headers=headers, **params)\n    return pages\n\ndef get_issues_list(project, auth=False, **params):\n    \"\"\"get issues list\"\"\"\n    params.setdefault(\"state\", \"closed\")\n    url = \"https://api.github.com/repos/{project}/issues\".format(project=project)\n    if auth:\n        headers = make_auth_header()\n    else:\n        headers = None\n    pages = get_paged_request(url, headers=headers, **params)\n    return pages\n\ndef get_milestones(project, auth=False, **params):\n    params.setdefault('state', 'all')\n    url = \"https://api.github.com/repos/{project}/milestones\".format(project=project)\n    if auth:\n        headers = make_auth_header()\n    else:\n        headers = None\n    milestones = get_paged_request(url, headers=headers, **params)\n    return milestones\n\ndef get_milestone_id(project, milestone, auth=False, **params):\n    milestones = get_milestones(project, auth=auth, **params)\n    for mstone in milestones:\n        if mstone['title'] == milestone:\n            return mstone['number']\n    else:\n        raise ValueError(\"milestone %s not found\" % milestone)\n\ndef is_pull_request(issue):\n    \"\"\"Return True if the given issue is a pull request.\"\"\"\n    return bool(issue.get('pull_request', {}).get('html_url', None))\n\ndef get_authors(pr):\n    print(\"getting authors for #%i\" % pr['number'], file=sys.stderr)\n    h = make_auth_header()\n    r = requests.get(pr['commits_url'], headers=h)\n    r.raise_for_status()\n    commits = r.json()\n    authors = []\n    for commit in commits:\n        author = commit['commit']['author']\n        authors.append(\"%s <%s>\" % (author['name'], author['email']))\n    return authors\n\n# encode_multipart_formdata is from urllib3.filepost\n# The only change is to iter_fields, to enforce S3's required key ordering\n\ndef iter_fields(fields):\n    fields = fields.copy()\n    for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',\n        'Policy', 'Signature', 'Content-Type', 'file'):\n        yield (key, fields.pop(key))\n    for (k,v) in fields.items():\n        yield k,v\n\ndef encode_multipart_formdata(fields, boundary=None):\n    \"\"\"\n    Encode a dictionary of ``fields`` using the multipart/form-data mime format.\n\n    :param fields:\n        Dictionary of fields or list of (key, value) field tuples.  The key is\n        treated as the field name, and the value as the body of the form-data\n        bytes. If the value is a tuple of two elements, then the first element\n        is treated as the filename of the form-data section.\n\n        Field names and filenames must be unicode.\n\n    :param boundary:\n        If not specified, then a random boundary will be generated using\n        :func:`mimetools.choose_boundary`.\n    \"\"\"\n    # copy requests imports in here:\n    from io import BytesIO\n    from requests.packages.urllib3.filepost import (\n        choose_boundary, six, writer, b, get_content_type\n    )\n    body = BytesIO()\n    if boundary is None:\n        boundary = choose_boundary()\n\n    for fieldname, value in iter_fields(fields):\n        body.write(b('--%s\\r\\n' % (boundary)))\n\n        if isinstance(value, tuple):\n            filename, data = value\n            writer(body).write('Content-Disposition: form-data; name=\"%s\"; '\n                               'filename=\"%s\"\\r\\n' % (fieldname, filename))\n            body.write(b('Content-Type: %s\\r\\n\\r\\n' %\n                       (get_content_type(filename))))\n        else:\n            data = value\n            writer(body).write('Content-Disposition: form-data; name=\"%s\"\\r\\n'\n                               % (fieldname))\n            body.write(b'Content-Type: text/plain\\r\\n\\r\\n')\n\n        if isinstance(data, int):\n            data = str(data)  # Backwards compatibility\n        if isinstance(data, six.text_type):\n            writer(body).write(data)\n        else:\n            body.write(data)\n\n        body.write(b'\\r\\n')\n\n    body.write(b('--%s--\\r\\n' % (boundary)))\n\n    content_type = b('multipart/form-data; boundary=%s' % boundary)\n\n    return body.getvalue(), content_type\n\n\ndef post_download(project, filename, name=None, description=\"\"):\n    \"\"\"Upload a file to the GitHub downloads area\"\"\"\n    if name is None:\n        name = os.path.basename(filename)\n    with open(filename, 'rb') as f:\n        filedata = f.read()\n\n    url = \"https://api.github.com/repos/{project}/downloads\".format(project=project)\n\n    payload = json.dumps(dict(name=name, size=len(filedata),\n                    description=description))\n    response = requests.post(url, data=payload, headers=make_auth_header())\n    response.raise_for_status()\n    reply = json.loads(response.content)\n    s3_url = reply['s3_url']\n\n    fields = dict(\n        key=reply['path'],\n        acl=reply['acl'],\n        success_action_status=201,\n        Filename=reply['name'],\n        AWSAccessKeyId=reply['accesskeyid'],\n        Policy=reply['policy'],\n        Signature=reply['signature'],\n        file=(reply['name'], filedata),\n    )\n    fields['Content-Type'] = reply['mime_type']\n    data, content_type = encode_multipart_formdata(fields)\n    s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})\n    return s3r\n", "path": "tools/gh_api.py"}], "after_files": [{"content": "\"\"\"Functions for Github API requests.\"\"\"\nfrom __future__ import print_function\n\ntry:\n    input = raw_input\nexcept NameError:\n    pass\n\nimport os\nimport re\nimport sys\n\nimport requests\nimport getpass\nimport json\n\ntry:\n    import requests_cache\nexcept ImportError:\n    print(\"no cache\", file=sys.stderr)\nelse:\n    requests_cache.install_cache(\"gh_api\", expire_after=3600)\n\n# Keyring stores passwords by a 'username', but we're not storing a username and\n# password\nfake_username = 'ipython_tools'\n\nclass Obj(dict):\n    \"\"\"Dictionary with attribute access to names.\"\"\"\n    def __getattr__(self, name):\n        try:\n            return self[name]\n        except KeyError:\n            raise AttributeError(name)\n\n    def __setattr__(self, name, val):\n        self[name] = val\n\ntoken = None\ndef get_auth_token():\n    global token\n\n    if token is not None:\n        return token\n\n    import keyring\n    token = keyring.get_password('github', fake_username)\n    if token is not None:\n        return token\n\n    print(\"Please enter your github username and password. These are not \"\n           \"stored, only used to get an oAuth token. You can revoke this at \"\n           \"any time on Github.\")\n    user = input(\"Username: \")\n    pw = getpass.getpass(\"Password: \")\n\n    auth_request = {\n      \"scopes\": [\n        \"public_repo\",\n        \"gist\"\n      ],\n      \"note\": \"IPython tools\",\n      \"note_url\": \"https://github.com/ipython/ipython/tree/master/tools\",\n    }\n    response = requests.post('https://api.github.com/authorizations',\n                            auth=(user, pw), data=json.dumps(auth_request))\n    if response.status_code == 401 and response.headers.get('X-GitHub-OTP') == 'required; sms':\n        print(\"Your login API resquest a SMS one time password\")\n        sms_pw = getpass.getpass(\"SMS password: \")\n        response = requests.post('https://api.github.com/authorizations',\n                            auth=(user, pw), \n                            data=json.dumps(auth_request),\n                            headers={'X-GitHub-OTP':sms_pw})\n    response.raise_for_status()\n    token = json.loads(response.text)['token']\n    keyring.set_password('github', fake_username, token)\n    return token\n\ndef make_auth_header():\n    return {'Authorization': 'token ' + get_auth_token()}\n\ndef post_issue_comment(project, num, body):\n    url = 'https://api.github.com/repos/{project}/issues/{num}/comments'.format(project=project, num=num)\n    payload = json.dumps({'body': body})\n    requests.post(url, data=payload, headers=make_auth_header())\n\ndef post_gist(content, description='', filename='file', auth=False):\n    \"\"\"Post some text to a Gist, and return the URL.\"\"\"\n    post_data = json.dumps({\n      \"description\": description,\n      \"public\": True,\n      \"files\": {\n        filename: {\n          \"content\": content\n        }\n      }\n    }).encode('utf-8')\n\n    headers = make_auth_header() if auth else {}\n    response = requests.post(\"https://api.github.com/gists\", data=post_data, headers=headers)\n    response.raise_for_status()\n    response_data = json.loads(response.text)\n    return response_data['html_url']\n\ndef get_pull_request(project, num, auth=False):\n    \"\"\"get pull request info  by number\n    \"\"\"\n    url = \"https://api.github.com/repos/{project}/pulls/{num}\".format(project=project, num=num)\n    if auth:\n        header = make_auth_header()\n    else:\n        header = None\n    print(\"fetching %s\" % url, file=sys.stderr)\n    response = requests.get(url, headers=header)\n    response.raise_for_status()\n    return json.loads(response.text, object_hook=Obj)\n\ndef get_pull_request_files(project, num, auth=False):\n    \"\"\"get list of files in a pull request\"\"\"\n    url = \"https://api.github.com/repos/{project}/pulls/{num}/files\".format(project=project, num=num)\n    if auth:\n        header = make_auth_header()\n    else:\n        header = None\n    return get_paged_request(url, headers=header)\n\nelement_pat = re.compile(r'<(.+?)>')\nrel_pat = re.compile(r'rel=[\\'\"](\\w+)[\\'\"]')\n\ndef get_paged_request(url, headers=None, **params):\n    \"\"\"get a full list, handling APIv3's paging\"\"\"\n    results = []\n    params.setdefault(\"per_page\", 100)\n    while True:\n        if '?' in url:\n            params = None\n            print(\"fetching %s\" % url, file=sys.stderr)\n        else:\n            print(\"fetching %s with %s\" % (url, params), file=sys.stderr)\n        response = requests.get(url, headers=headers, params=params)\n        response.raise_for_status()\n        results.extend(response.json())\n        if 'next' in response.links:\n            url = response.links['next']['url']\n        else:\n            break\n    return results\n\ndef get_pulls_list(project, auth=False, **params):\n    \"\"\"get pull request list\"\"\"\n    params.setdefault(\"state\", \"closed\")\n    url = \"https://api.github.com/repos/{project}/pulls\".format(project=project)\n    if auth:\n        headers = make_auth_header()\n    else:\n        headers = None\n    pages = get_paged_request(url, headers=headers, **params)\n    return pages\n\ndef get_issues_list(project, auth=False, **params):\n    \"\"\"get issues list\"\"\"\n    params.setdefault(\"state\", \"closed\")\n    url = \"https://api.github.com/repos/{project}/issues\".format(project=project)\n    if auth:\n        headers = make_auth_header()\n    else:\n        headers = None\n    pages = get_paged_request(url, headers=headers, **params)\n    return pages\n\ndef get_milestones(project, auth=False, **params):\n    params.setdefault('state', 'all')\n    url = \"https://api.github.com/repos/{project}/milestones\".format(project=project)\n    if auth:\n        headers = make_auth_header()\n    else:\n        headers = None\n    milestones = get_paged_request(url, headers=headers, **params)\n    return milestones\n\ndef get_milestone_id(project, milestone, auth=False, **params):\n    milestones = get_milestones(project, auth=auth, **params)\n    for mstone in milestones:\n        if mstone['title'] == milestone:\n            return mstone['number']\n    else:\n        raise ValueError(\"milestone %s not found\" % milestone)\n\ndef is_pull_request(issue):\n    \"\"\"Return True if the given issue is a pull request.\"\"\"\n    return bool(issue.get('pull_request', {}).get('html_url', None))\n\ndef get_authors(pr):\n    print(\"getting authors for #%i\" % pr['number'], file=sys.stderr)\n    h = make_auth_header()\n    r = requests.get(pr['commits_url'], headers=h)\n    r.raise_for_status()\n    commits = r.json()\n    authors = []\n    for commit in commits:\n        author = commit['commit']['author']\n        authors.append(\"%s <%s>\" % (author['name'], author['email']))\n    return authors\n\n# encode_multipart_formdata is from urllib3.filepost\n# The only change is to iter_fields, to enforce S3's required key ordering\n\ndef iter_fields(fields):\n    fields = fields.copy()\n    for key in ('key', 'acl', 'Filename', 'success_action_status', 'AWSAccessKeyId',\n        'Policy', 'Signature', 'Content-Type', 'file'):\n        yield (key, fields.pop(key))\n    for (k,v) in fields.items():\n        yield k,v\n\ndef encode_multipart_formdata(fields, boundary=None):\n    \"\"\"\n    Encode a dictionary of ``fields`` using the multipart/form-data mime format.\n\n    :param fields:\n        Dictionary of fields or list of (key, value) field tuples.  The key is\n        treated as the field name, and the value as the body of the form-data\n        bytes. If the value is a tuple of two elements, then the first element\n        is treated as the filename of the form-data section.\n\n        Field names and filenames must be unicode.\n\n    :param boundary:\n        If not specified, then a random boundary will be generated using\n        :func:`mimetools.choose_boundary`.\n    \"\"\"\n    # copy requests imports in here:\n    from io import BytesIO\n    from requests.packages.urllib3.filepost import (\n        choose_boundary, six, writer, b, get_content_type\n    )\n    body = BytesIO()\n    if boundary is None:\n        boundary = choose_boundary()\n\n    for fieldname, value in iter_fields(fields):\n        body.write(b('--%s\\r\\n' % (boundary)))\n\n        if isinstance(value, tuple):\n            filename, data = value\n            writer(body).write('Content-Disposition: form-data; name=\"%s\"; '\n                               'filename=\"%s\"\\r\\n' % (fieldname, filename))\n            body.write(b('Content-Type: %s\\r\\n\\r\\n' %\n                       (get_content_type(filename))))\n        else:\n            data = value\n            writer(body).write('Content-Disposition: form-data; name=\"%s\"\\r\\n'\n                               % (fieldname))\n            body.write(b'Content-Type: text/plain\\r\\n\\r\\n')\n\n        if isinstance(data, int):\n            data = str(data)  # Backwards compatibility\n        if isinstance(data, six.text_type):\n            writer(body).write(data)\n        else:\n            body.write(data)\n\n        body.write(b'\\r\\n')\n\n    body.write(b('--%s--\\r\\n' % (boundary)))\n\n    content_type = b('multipart/form-data; boundary=%s' % boundary)\n\n    return body.getvalue(), content_type\n\n\ndef post_download(project, filename, name=None, description=\"\"):\n    \"\"\"Upload a file to the GitHub downloads area\"\"\"\n    if name is None:\n        name = os.path.basename(filename)\n    with open(filename, 'rb') as f:\n        filedata = f.read()\n\n    url = \"https://api.github.com/repos/{project}/downloads\".format(project=project)\n\n    payload = json.dumps(dict(name=name, size=len(filedata),\n                    description=description))\n    response = requests.post(url, data=payload, headers=make_auth_header())\n    response.raise_for_status()\n    reply = json.loads(response.content)\n    s3_url = reply['s3_url']\n\n    fields = dict(\n        key=reply['path'],\n        acl=reply['acl'],\n        success_action_status=201,\n        Filename=reply['name'],\n        AWSAccessKeyId=reply['accesskeyid'],\n        Policy=reply['policy'],\n        Signature=reply['signature'],\n        file=(reply['name'], filedata),\n    )\n    fields['Content-Type'] = reply['mime_type']\n    data, content_type = encode_multipart_formdata(fields)\n    s3r = requests.post(s3_url, data=data, headers={'Content-Type': content_type})\n    return s3r\n", "path": "tools/gh_api.py"}]}