We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
use uuid instead of username for Bitbucket
**Update:** Bitbucket recently rolled out `uuid`s for their user API, so we can switch to that instead of dropping them. See [below](https://github.com/gratipay/gratipay.com/issues/1945#issuecomment-62242306).

---

In the long run we're considering dropping all authentication via OAuth and only allowing password authentication (#1052). In the short run we need to drop Bitbucket as an auth method, because Bitbucket gives us no immutable user id, so if someone deletes their Bitbucket account, an attacker could create a Bitbucket account with their username and take over their Gittip account. @bruceadams demonstrated this here: https://github.com/gittip/www.gittip.com/issues/1807#issuecomment-33314863.

<bountysource-plugin>

---

Want to back this issue? **[Place a bounty on it!](https://www.bountysource.com/issues/1387030-drop-bitbucket-as-auth-method?utm_campaign=plugin&utm_content=tracker%2F85909&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F85909&utm_medium=issues&utm_source=github).
</bountysource-plugin>


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `gratipay/elsewhere/__init__.py`
Content:
```
1 """This subpackage contains functionality for working with accounts elsewhere.
2 """
3 from __future__ import division, print_function, unicode_literals
4 
5 from collections import OrderedDict
6 from datetime import datetime
7 import hashlib
8 import json
9 import logging
10 from urllib import quote
11 import xml.etree.ElementTree as ET
12 
13 from aspen import log, Response
14 from aspen.utils import to_age, utc
15 from requests_oauthlib import OAuth1Session, OAuth2Session
16 
17 from gratipay.elsewhere._extractors import not_available
18 
19 
20 ACTIONS = {'opt-in', 'connect', 'lock', 'unlock'}
21 PLATFORMS = 'facebook google bitbucket bountysource github openstreetmap twitter venmo'.split()
22 
23 
24 class UnknownAccountElsewhere(Exception): pass
25 
26 
27 class PlatformRegistry(object):
28     """Registry of platforms we support connecting to Gratipay accounts.
29     """
30     def __init__(self, platforms):
31         self.__dict__ = OrderedDict((p.name, p) for p in platforms)
32 
33     def __contains__(self, platform):
34         return platform.name in self.__dict__
35 
36     def __iter__(self):
37         return iter(self.__dict__.values())
38 
39 
40 class UserInfo(object):
41     """A simple container for a user's info.
42 
43     Accessing a non-existing attribute returns `None`.
44     """
45 
46     def __init__(self, **kw):
47         self.__dict__.update(kw)
48 
49     def __getattr__(self, key):
50         return self.__dict__.get(key, None)
51 
52     def __setattr__(self, key, value):
53         if value is None:
54             self.__dict__.pop(key, None)
55         else:
56             self.__dict__[key] = value
57 
58 
59 class Platform(object):
60 
61     allows_team_connect = False
62 
63     # "x" stands for "extract"
64     x_user_info = not_available
65     x_user_id = not_available
66     x_user_name = not_available
67     x_display_name = not_available
68     x_email = not_available
69     x_gravatar_id = not_available
70     x_avatar_url = not_available
71     x_is_team = not_available
72 
73     required_attrs = ( 'account_url'
74                      , 'display_name'
75                      , 'name'
76                      )
77 
78     def __init__(self, api_key, api_secret, callback_url, api_url=None, auth_url=None):
79         self.api_key = api_key
80         self.api_secret = api_secret
81         self.callback_url = callback_url
82         if api_url:
83             self.api_url = api_url
84         if auth_url:
85             self.auth_url = auth_url
86         elif not getattr(self, 'auth_url', None):
87             self.auth_url = self.api_url
88 
89         # Determine the appropriate response parser using `self.api_format`
90         api_format = getattr(self, 'api_format', None)
91         if api_format == 'json':
92             self.api_parser = lambda r: r.json()
93         elif api_format == 'xml':
94             self.api_parser = lambda r: ET.fromstring(r.content)
95         elif api_format:
96             raise ValueError('unknown API format: '+str(api_format))
97 
98         # Make sure the subclass was implemented properly.
99         missing_attrs = [a for a in self.required_attrs if not hasattr(self, a)]
100         if missing_attrs:
101             msg = "The class %s is missing these required attributes: %s"
102             msg %= self.__class__.__name__, ', '.join(missing_attrs)
103             raise AttributeError(msg)
104 
105     def api_get(self, path, sess=None, **kw):
106         """
107         Given a `path` (e.g. /users/foo), this function sends a GET request to
108         the platform's API (e.g. https://api.github.com/users/foo).
109 
110         The response is returned, after checking its status code and ratelimit
111         headers.
112         """
113         if not sess:
114             sess = self.get_auth_session()
115         response = sess.get(self.api_url+path, **kw)
116 
117         # Check status
118         status = response.status_code
119         if status == 404:
120             raise Response(404)
121         elif status != 200:
122             log('{} api responded with {}:\n{}'.format(self.name, status, response.text)
123                , level=logging.ERROR)
124             raise Response(500, '{} lookup failed with {}'.format(self.name, status))
125 
126         # Check ratelimit headers
127         prefix = getattr(self, 'ratelimit_headers_prefix', None)
128         if prefix:
129             limit = response.headers[prefix+'limit']
130             remaining = response.headers[prefix+'remaining']
131             reset = response.headers[prefix+'reset']
132             try:
133                 limit, remaining, reset = int(limit), int(remaining), int(reset)
134             except (TypeError, ValueError):
135                 d = dict(limit=limit, remaining=remaining, reset=reset)
136                 log('Got weird rate headers from %s: %s' % (self.name, d))
137             else:
138                 percent_remaining = remaining/limit
139                 if percent_remaining < 0.5:
140                     reset = to_age(datetime.fromtimestamp(reset, tz=utc))
141                     log_msg = (
142                         '{0} API: {1:.1%} of ratelimit has been consumed, '
143                         '{2} requests remaining, resets {3}.'
144                     ).format(self.name, 1 - percent_remaining, remaining, reset)
145                     log_lvl = logging.WARNING
146                     if percent_remaining < 0.2:
147                         log_lvl = logging.ERROR
148                     elif percent_remaining < 0.05:
149                         log_lvl = logging.CRITICAL
150                     log(log_msg, log_lvl)
151 
152         return response
153 
154     def extract_user_info(self, info):
155         """
156         Given a user_info object of variable type (depending on the platform),
157         extract the relevant information by calling the platform's extractors
158         (`x_user_name`, `x_user_id`, etc).
159 
160         Returns a `UserInfo`. The `user_id` attribute is guaranteed to have a
161         unique non-empty value.
162         """
163         r = UserInfo(platform=self.name)
164         info = self.x_user_info(r, info, info)
165         r.user_name = self.x_user_name(r, info, None)
166         if self.x_user_id.__func__ is not_available:
167             r.user_id = r.user_name
168         else:
169             r.user_id = self.x_user_id(r, info)
170         assert r.user_id is not None
171         r.user_id = unicode(r.user_id)
172         assert len(r.user_id) > 0
173         r.display_name = self.x_display_name(r, info, None)
174         r.email = self.x_email(r, info, None)
175         r.avatar_url = self.x_avatar_url(r, info, None)
176         if not r.avatar_url:
177             gravatar_id = self.x_gravatar_id(r, info, None)
178             if r.email and not gravatar_id:
179                 gravatar_id = hashlib.md5(r.email.strip().lower()).hexdigest()
180             if gravatar_id:
181                 r.avatar_url = 'https://secure.gravatar.com/avatar/'+gravatar_id
182         r.is_team = self.x_is_team(r, info, False)
183         r.extra_info = info
184         return r
185 
186     def get_team_members(self, team_name, page_url=None):
187         """Given a team_name on the platform, return the team's membership list
188         from the API.
189         """
190         default_url = self.api_team_members_path.format(user_name=quote(team_name))
191         r = self.api_get(page_url or default_url)
192         members, count, pages_urls = self.api_paginator(r, self.api_parser(r))
193         members = [self.extract_user_info(m) for m in members]
194         return members, count, pages_urls
195 
196     def get_user_info(self, user_name, sess=None):
197         """Given a user_name on the platform, get the user's info from the API.
198         """
199         try:
200             path = self.api_user_info_path.format(user_name=quote(user_name))
201         except KeyError:
202             raise Response(404)
203         info = self.api_parser(self.api_get(path, sess=sess))
204         return self.extract_user_info(info)
205 
206     def get_user_self_info(self, sess):
207         """Get the authenticated user's info from the API.
208         """
209         r = self.api_get(self.api_user_self_info_path, sess=sess)
210         info = self.extract_user_info(self.api_parser(r))
211         token = getattr(sess, 'token', None)
212         if token:
213             info.token = json.dumps(token)
214         return info
215 
216 
217 class PlatformOAuth1(Platform):
218 
219     request_token_path = '/oauth/request_token'
220     authorize_path = '/oauth/authorize'
221     access_token_path = '/oauth/access_token'
222 
223     def get_auth_session(self, token=None):
224         args = ()
225         if token:
226             args = (token['token'], token['token_secret'])
227         return OAuth1Session(self.api_key, self.api_secret, *args,
228                              callback_uri=self.callback_url)
229 
230     def get_auth_url(self, **kw):
231         sess = self.get_auth_session()
232         r = sess.fetch_request_token(self.auth_url+self.request_token_path)
233         url = sess.authorization_url(self.auth_url+self.authorize_path)
234         return url, r['oauth_token'], r['oauth_token_secret']
235 
236     def get_query_id(self, querystring):
237         return querystring['oauth_token']
238 
239     def handle_auth_callback(self, url, token, token_secret):
240         sess = self.get_auth_session(dict(token=token, token_secret=token_secret))
241         sess.parse_authorization_response(url)
242         r = sess.fetch_access_token(self.auth_url+self.access_token_path)
243         sess.token = dict(token=r['oauth_token'],
244                           token_secret=r['oauth_token_secret'])
245         return sess
246 
247 
248 class PlatformOAuth2(Platform):
249 
250     oauth_default_scope = None
251     oauth_email_scope = None
252     oauth_payment_scope = None
253 
254     def get_auth_session(self, state=None, token=None, token_updater=None):
255         return OAuth2Session(self.api_key, state=state, token=token,
256                              token_updater=token_updater,
257                              redirect_uri=self.callback_url,
258                              scope=self.oauth_default_scope)
259 
260     def get_auth_url(self, **kw):
261         sess = self.get_auth_session()
262         url, state = sess.authorization_url(self.auth_url)
263         return url, state, ''
264 
265     def get_query_id(self, querystring):
266         return querystring['state']
267 
268     def handle_auth_callback(self, url, state, unused_arg):
269         sess = self.get_auth_session(state=state)
270         sess.fetch_token(self.access_token_url,
271                          client_secret=self.api_secret,
272                          authorization_response=url)
273         return sess
274
```

Path: `gratipay/elsewhere/bitbucket.py`
Content:
```
1 from __future__ import absolute_import, division, print_function, unicode_literals
2 
3 from gratipay.elsewhere import PlatformOAuth1
4 from gratipay.elsewhere._extractors import any_key, key, not_available
5 from gratipay.elsewhere._paginators import keys_paginator
6 
7 
8 class Bitbucket(PlatformOAuth1):
9 
10     # Platform attributes
11     name = 'bitbucket'
12     display_name = 'Bitbucket'
13     account_url = 'https://bitbucket.org/{user_name}'
14 
15     # Auth attributes
16     auth_url = 'https://bitbucket.org/api/1.0'
17     authorize_path = '/oauth/authenticate'
18 
19     # API attributes
20     api_format = 'json'
21     api_paginator = keys_paginator(prev='previous')
22     api_url = 'https://bitbucket.org/api'
23     api_user_info_path = '/1.0/users/{user_name}'
24     api_user_self_info_path = '/1.0/user'
25     api_team_members_path = '/2.0/teams/{user_name}/members'
26 
27     # User info extractors
28     x_user_info = key('user')
29     x_user_id = not_available  # No immutable id. :-/
30     x_user_name = key('username')
31     x_display_name = key('display_name')
32     x_email = not_available
33     x_avatar_url = any_key('avatar', ('links', 'avatar', 'href'))
34     x_is_team = key('is_team')
35
```
--- 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/gratipay/elsewhere/__init__.py b/gratipay/elsewhere/__init__.py
--- a/gratipay/elsewhere/__init__.py
+++ b/gratipay/elsewhere/__init__.py
@@ -117,7 +117,7 @@
         # Check status
         status = response.status_code
         if status == 404:
-            raise Response(404)
+            raise Response(404, response.text)
         elif status != 200:
             log('{} api responded with {}:\n{}'.format(self.name, status, response.text)
                , level=logging.ERROR)
diff --git a/gratipay/elsewhere/bitbucket.py b/gratipay/elsewhere/bitbucket.py
--- a/gratipay/elsewhere/bitbucket.py
+++ b/gratipay/elsewhere/bitbucket.py
@@ -1,5 +1,6 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+from aspen import Response
 from gratipay.elsewhere import PlatformOAuth1
 from gratipay.elsewhere._extractors import any_key, key, not_available
 from gratipay.elsewhere._paginators import keys_paginator
@@ -20,15 +21,31 @@
     api_format = 'json'
     api_paginator = keys_paginator(prev='previous')
     api_url = 'https://bitbucket.org/api'
-    api_user_info_path = '/1.0/users/{user_name}'
-    api_user_self_info_path = '/1.0/user'
+    api_user_info_path = '/2.0/users/{user_name}'
+    api_user_self_info_path = '/2.0/user'
     api_team_members_path = '/2.0/teams/{user_name}/members'
 
     # User info extractors
     x_user_info = key('user')
-    x_user_id = not_available  # No immutable id. :-/
+    x_user_id = key('uuid')
     x_user_name = key('username')
     x_display_name = key('display_name')
     x_email = not_available
     x_avatar_url = any_key('avatar', ('links', 'avatar', 'href'))
-    x_is_team = key('is_team')
+    x_is_team = key('type', lambda v: v == 'team')
+
+    def api_get(self, path, sess=None, **kw):
+        """Extend to manually retry /users/pypy as /teams/pypy.
+
+        Bitbucket gives us a 404 where a 30x would be more helpful.
+
+        """
+        try:
+            return PlatformOAuth1.api_get(self, path, sess, **kw)
+        except Response, response:
+            if response.code == 404 and ' is a team account' in response.body:
+                assert path.startswith('/2.0/users/')
+                path = '/2.0/teams/' + path[11:]
+                return PlatformOAuth1.api_get(self, path, sess, **kw)
+            else:
+                raise

{"golden_diff": "diff --git a/gratipay/elsewhere/__init__.py b/gratipay/elsewhere/__init__.py\n--- a/gratipay/elsewhere/__init__.py\n+++ b/gratipay/elsewhere/__init__.py\n@@ -117,7 +117,7 @@\n         # Check status\n         status = response.status_code\n         if status == 404:\n-            raise Response(404)\n+            raise Response(404, response.text)\n         elif status != 200:\n             log('{} api responded with {}:\\n{}'.format(self.name, status, response.text)\n                , level=logging.ERROR)\ndiff --git a/gratipay/elsewhere/bitbucket.py b/gratipay/elsewhere/bitbucket.py\n--- a/gratipay/elsewhere/bitbucket.py\n+++ b/gratipay/elsewhere/bitbucket.py\n@@ -1,5 +1,6 @@\n from __future__ import absolute_import, division, print_function, unicode_literals\n \n+from aspen import Response\n from gratipay.elsewhere import PlatformOAuth1\n from gratipay.elsewhere._extractors import any_key, key, not_available\n from gratipay.elsewhere._paginators import keys_paginator\n@@ -20,15 +21,31 @@\n     api_format = 'json'\n     api_paginator = keys_paginator(prev='previous')\n     api_url = 'https://bitbucket.org/api'\n-    api_user_info_path = '/1.0/users/{user_name}'\n-    api_user_self_info_path = '/1.0/user'\n+    api_user_info_path = '/2.0/users/{user_name}'\n+    api_user_self_info_path = '/2.0/user'\n     api_team_members_path = '/2.0/teams/{user_name}/members'\n \n     # User info extractors\n     x_user_info = key('user')\n-    x_user_id = not_available  # No immutable id. :-/\n+    x_user_id = key('uuid')\n     x_user_name = key('username')\n     x_display_name = key('display_name')\n     x_email = not_available\n     x_avatar_url = any_key('avatar', ('links', 'avatar', 'href'))\n-    x_is_team = key('is_team')\n+    x_is_team = key('type', lambda v: v == 'team')\n+\n+    def api_get(self, path, sess=None, **kw):\n+        \"\"\"Extend to manually retry /users/pypy as /teams/pypy.\n+\n+        Bitbucket gives us a 404 where a 30x would be more helpful.\n+\n+        \"\"\"\n+        try:\n+            return PlatformOAuth1.api_get(self, path, sess, **kw)\n+        except Response, response:\n+            if response.code == 404 and ' is a team account' in response.body:\n+                assert path.startswith('/2.0/users/')\n+                path = '/2.0/teams/' + path[11:]\n+                return PlatformOAuth1.api_get(self, path, sess, **kw)\n+            else:\n+                raise\n", "issue": "use uuid instead of username for Bitbucket\n**Update:** Bitbucket recently rolled out `uuid`s for their user API, so we can switch to that instead of dropping them. See [below](https://github.com/gratipay/gratipay.com/issues/1945#issuecomment-62242306).\n\n---\n\nIn the long run we're considering dropping all authentication via OAuth and only allowing password authentication (#1052). In the short run we need to drop Bitbucket as an auth method, because Bitbucket gives us no immutable user id, so if someone deletes their Bitbucket account, an attacker could create a Bitbucket account with their username and take over their Gittip account. @bruceadams demonstrated this here: https://github.com/gittip/www.gittip.com/issues/1807#issuecomment-33314863.\n\n<bountysource-plugin>\n\n---\n\nWant to back this issue? **[Place a bounty on it!](https://www.bountysource.com/issues/1387030-drop-bitbucket-as-auth-method?utm_campaign=plugin&utm_content=tracker%2F85909&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F85909&utm_medium=issues&utm_source=github).\n</bountysource-plugin>\n\n", "before_files": [{"content": "\"\"\"This subpackage contains functionality for working with accounts elsewhere.\n\"\"\"\nfrom __future__ import division, print_function, unicode_literals\n\nfrom collections import OrderedDict\nfrom datetime import datetime\nimport hashlib\nimport json\nimport logging\nfrom urllib import quote\nimport xml.etree.ElementTree as ET\n\nfrom aspen import log, Response\nfrom aspen.utils import to_age, utc\nfrom requests_oauthlib import OAuth1Session, OAuth2Session\n\nfrom gratipay.elsewhere._extractors import not_available\n\n\nACTIONS = {'opt-in', 'connect', 'lock', 'unlock'}\nPLATFORMS = 'facebook google bitbucket bountysource github openstreetmap twitter venmo'.split()\n\n\nclass UnknownAccountElsewhere(Exception): pass\n\n\nclass PlatformRegistry(object):\n    \"\"\"Registry of platforms we support connecting to Gratipay accounts.\n    \"\"\"\n    def __init__(self, platforms):\n        self.__dict__ = OrderedDict((p.name, p) for p in platforms)\n\n    def __contains__(self, platform):\n        return platform.name in self.__dict__\n\n    def __iter__(self):\n        return iter(self.__dict__.values())\n\n\nclass UserInfo(object):\n    \"\"\"A simple container for a user's info.\n\n    Accessing a non-existing attribute returns `None`.\n    \"\"\"\n\n    def __init__(self, **kw):\n        self.__dict__.update(kw)\n\n    def __getattr__(self, key):\n        return self.__dict__.get(key, None)\n\n    def __setattr__(self, key, value):\n        if value is None:\n            self.__dict__.pop(key, None)\n        else:\n            self.__dict__[key] = value\n\n\nclass Platform(object):\n\n    allows_team_connect = False\n\n    # \"x\" stands for \"extract\"\n    x_user_info = not_available\n    x_user_id = not_available\n    x_user_name = not_available\n    x_display_name = not_available\n    x_email = not_available\n    x_gravatar_id = not_available\n    x_avatar_url = not_available\n    x_is_team = not_available\n\n    required_attrs = ( 'account_url'\n                     , 'display_name'\n                     , 'name'\n                     )\n\n    def __init__(self, api_key, api_secret, callback_url, api_url=None, auth_url=None):\n        self.api_key = api_key\n        self.api_secret = api_secret\n        self.callback_url = callback_url\n        if api_url:\n            self.api_url = api_url\n        if auth_url:\n            self.auth_url = auth_url\n        elif not getattr(self, 'auth_url', None):\n            self.auth_url = self.api_url\n\n        # Determine the appropriate response parser using `self.api_format`\n        api_format = getattr(self, 'api_format', None)\n        if api_format == 'json':\n            self.api_parser = lambda r: r.json()\n        elif api_format == 'xml':\n            self.api_parser = lambda r: ET.fromstring(r.content)\n        elif api_format:\n            raise ValueError('unknown API format: '+str(api_format))\n\n        # Make sure the subclass was implemented properly.\n        missing_attrs = [a for a in self.required_attrs if not hasattr(self, a)]\n        if missing_attrs:\n            msg = \"The class %s is missing these required attributes: %s\"\n            msg %= self.__class__.__name__, ', '.join(missing_attrs)\n            raise AttributeError(msg)\n\n    def api_get(self, path, sess=None, **kw):\n        \"\"\"\n        Given a `path` (e.g. /users/foo), this function sends a GET request to\n        the platform's API (e.g. https://api.github.com/users/foo).\n\n        The response is returned, after checking its status code and ratelimit\n        headers.\n        \"\"\"\n        if not sess:\n            sess = self.get_auth_session()\n        response = sess.get(self.api_url+path, **kw)\n\n        # Check status\n        status = response.status_code\n        if status == 404:\n            raise Response(404)\n        elif status != 200:\n            log('{} api responded with {}:\\n{}'.format(self.name, status, response.text)\n               , level=logging.ERROR)\n            raise Response(500, '{} lookup failed with {}'.format(self.name, status))\n\n        # Check ratelimit headers\n        prefix = getattr(self, 'ratelimit_headers_prefix', None)\n        if prefix:\n            limit = response.headers[prefix+'limit']\n            remaining = response.headers[prefix+'remaining']\n            reset = response.headers[prefix+'reset']\n            try:\n                limit, remaining, reset = int(limit), int(remaining), int(reset)\n            except (TypeError, ValueError):\n                d = dict(limit=limit, remaining=remaining, reset=reset)\n                log('Got weird rate headers from %s: %s' % (self.name, d))\n            else:\n                percent_remaining = remaining/limit\n                if percent_remaining < 0.5:\n                    reset = to_age(datetime.fromtimestamp(reset, tz=utc))\n                    log_msg = (\n                        '{0} API: {1:.1%} of ratelimit has been consumed, '\n                        '{2} requests remaining, resets {3}.'\n                    ).format(self.name, 1 - percent_remaining, remaining, reset)\n                    log_lvl = logging.WARNING\n                    if percent_remaining < 0.2:\n                        log_lvl = logging.ERROR\n                    elif percent_remaining < 0.05:\n                        log_lvl = logging.CRITICAL\n                    log(log_msg, log_lvl)\n\n        return response\n\n    def extract_user_info(self, info):\n        \"\"\"\n        Given a user_info object of variable type (depending on the platform),\n        extract the relevant information by calling the platform's extractors\n        (`x_user_name`, `x_user_id`, etc).\n\n        Returns a `UserInfo`. The `user_id` attribute is guaranteed to have a\n        unique non-empty value.\n        \"\"\"\n        r = UserInfo(platform=self.name)\n        info = self.x_user_info(r, info, info)\n        r.user_name = self.x_user_name(r, info, None)\n        if self.x_user_id.__func__ is not_available:\n            r.user_id = r.user_name\n        else:\n            r.user_id = self.x_user_id(r, info)\n        assert r.user_id is not None\n        r.user_id = unicode(r.user_id)\n        assert len(r.user_id) > 0\n        r.display_name = self.x_display_name(r, info, None)\n        r.email = self.x_email(r, info, None)\n        r.avatar_url = self.x_avatar_url(r, info, None)\n        if not r.avatar_url:\n            gravatar_id = self.x_gravatar_id(r, info, None)\n            if r.email and not gravatar_id:\n                gravatar_id = hashlib.md5(r.email.strip().lower()).hexdigest()\n            if gravatar_id:\n                r.avatar_url = 'https://secure.gravatar.com/avatar/'+gravatar_id\n        r.is_team = self.x_is_team(r, info, False)\n        r.extra_info = info\n        return r\n\n    def get_team_members(self, team_name, page_url=None):\n        \"\"\"Given a team_name on the platform, return the team's membership list\n        from the API.\n        \"\"\"\n        default_url = self.api_team_members_path.format(user_name=quote(team_name))\n        r = self.api_get(page_url or default_url)\n        members, count, pages_urls = self.api_paginator(r, self.api_parser(r))\n        members = [self.extract_user_info(m) for m in members]\n        return members, count, pages_urls\n\n    def get_user_info(self, user_name, sess=None):\n        \"\"\"Given a user_name on the platform, get the user's info from the API.\n        \"\"\"\n        try:\n            path = self.api_user_info_path.format(user_name=quote(user_name))\n        except KeyError:\n            raise Response(404)\n        info = self.api_parser(self.api_get(path, sess=sess))\n        return self.extract_user_info(info)\n\n    def get_user_self_info(self, sess):\n        \"\"\"Get the authenticated user's info from the API.\n        \"\"\"\n        r = self.api_get(self.api_user_self_info_path, sess=sess)\n        info = self.extract_user_info(self.api_parser(r))\n        token = getattr(sess, 'token', None)\n        if token:\n            info.token = json.dumps(token)\n        return info\n\n\nclass PlatformOAuth1(Platform):\n\n    request_token_path = '/oauth/request_token'\n    authorize_path = '/oauth/authorize'\n    access_token_path = '/oauth/access_token'\n\n    def get_auth_session(self, token=None):\n        args = ()\n        if token:\n            args = (token['token'], token['token_secret'])\n        return OAuth1Session(self.api_key, self.api_secret, *args,\n                             callback_uri=self.callback_url)\n\n    def get_auth_url(self, **kw):\n        sess = self.get_auth_session()\n        r = sess.fetch_request_token(self.auth_url+self.request_token_path)\n        url = sess.authorization_url(self.auth_url+self.authorize_path)\n        return url, r['oauth_token'], r['oauth_token_secret']\n\n    def get_query_id(self, querystring):\n        return querystring['oauth_token']\n\n    def handle_auth_callback(self, url, token, token_secret):\n        sess = self.get_auth_session(dict(token=token, token_secret=token_secret))\n        sess.parse_authorization_response(url)\n        r = sess.fetch_access_token(self.auth_url+self.access_token_path)\n        sess.token = dict(token=r['oauth_token'],\n                          token_secret=r['oauth_token_secret'])\n        return sess\n\n\nclass PlatformOAuth2(Platform):\n\n    oauth_default_scope = None\n    oauth_email_scope = None\n    oauth_payment_scope = None\n\n    def get_auth_session(self, state=None, token=None, token_updater=None):\n        return OAuth2Session(self.api_key, state=state, token=token,\n                             token_updater=token_updater,\n                             redirect_uri=self.callback_url,\n                             scope=self.oauth_default_scope)\n\n    def get_auth_url(self, **kw):\n        sess = self.get_auth_session()\n        url, state = sess.authorization_url(self.auth_url)\n        return url, state, ''\n\n    def get_query_id(self, querystring):\n        return querystring['state']\n\n    def handle_auth_callback(self, url, state, unused_arg):\n        sess = self.get_auth_session(state=state)\n        sess.fetch_token(self.access_token_url,\n                         client_secret=self.api_secret,\n                         authorization_response=url)\n        return sess\n", "path": "gratipay/elsewhere/__init__.py"}, {"content": "from __future__ import absolute_import, division, print_function, unicode_literals\n\nfrom gratipay.elsewhere import PlatformOAuth1\nfrom gratipay.elsewhere._extractors import any_key, key, not_available\nfrom gratipay.elsewhere._paginators import keys_paginator\n\n\nclass Bitbucket(PlatformOAuth1):\n\n    # Platform attributes\n    name = 'bitbucket'\n    display_name = 'Bitbucket'\n    account_url = 'https://bitbucket.org/{user_name}'\n\n    # Auth attributes\n    auth_url = 'https://bitbucket.org/api/1.0'\n    authorize_path = '/oauth/authenticate'\n\n    # API attributes\n    api_format = 'json'\n    api_paginator = keys_paginator(prev='previous')\n    api_url = 'https://bitbucket.org/api'\n    api_user_info_path = '/1.0/users/{user_name}'\n    api_user_self_info_path = '/1.0/user'\n    api_team_members_path = '/2.0/teams/{user_name}/members'\n\n    # User info extractors\n    x_user_info = key('user')\n    x_user_id = not_available  # No immutable id. :-/\n    x_user_name = key('username')\n    x_display_name = key('display_name')\n    x_email = not_available\n    x_avatar_url = any_key('avatar', ('links', 'avatar', 'href'))\n    x_is_team = key('is_team')\n", "path": "gratipay/elsewhere/bitbucket.py"}], "after_files": [{"content": "\"\"\"This subpackage contains functionality for working with accounts elsewhere.\n\"\"\"\nfrom __future__ import division, print_function, unicode_literals\n\nfrom collections import OrderedDict\nfrom datetime import datetime\nimport hashlib\nimport json\nimport logging\nfrom urllib import quote\nimport xml.etree.ElementTree as ET\n\nfrom aspen import log, Response\nfrom aspen.utils import to_age, utc\nfrom requests_oauthlib import OAuth1Session, OAuth2Session\n\nfrom gratipay.elsewhere._extractors import not_available\n\n\nACTIONS = {'opt-in', 'connect', 'lock', 'unlock'}\nPLATFORMS = 'facebook google bitbucket bountysource github openstreetmap twitter venmo'.split()\n\n\nclass UnknownAccountElsewhere(Exception): pass\n\n\nclass PlatformRegistry(object):\n    \"\"\"Registry of platforms we support connecting to Gratipay accounts.\n    \"\"\"\n    def __init__(self, platforms):\n        self.__dict__ = OrderedDict((p.name, p) for p in platforms)\n\n    def __contains__(self, platform):\n        return platform.name in self.__dict__\n\n    def __iter__(self):\n        return iter(self.__dict__.values())\n\n\nclass UserInfo(object):\n    \"\"\"A simple container for a user's info.\n\n    Accessing a non-existing attribute returns `None`.\n    \"\"\"\n\n    def __init__(self, **kw):\n        self.__dict__.update(kw)\n\n    def __getattr__(self, key):\n        return self.__dict__.get(key, None)\n\n    def __setattr__(self, key, value):\n        if value is None:\n            self.__dict__.pop(key, None)\n        else:\n            self.__dict__[key] = value\n\n\nclass Platform(object):\n\n    allows_team_connect = False\n\n    # \"x\" stands for \"extract\"\n    x_user_info = not_available\n    x_user_id = not_available\n    x_user_name = not_available\n    x_display_name = not_available\n    x_email = not_available\n    x_gravatar_id = not_available\n    x_avatar_url = not_available\n    x_is_team = not_available\n\n    required_attrs = ( 'account_url'\n                     , 'display_name'\n                     , 'name'\n                     )\n\n    def __init__(self, api_key, api_secret, callback_url, api_url=None, auth_url=None):\n        self.api_key = api_key\n        self.api_secret = api_secret\n        self.callback_url = callback_url\n        if api_url:\n            self.api_url = api_url\n        if auth_url:\n            self.auth_url = auth_url\n        elif not getattr(self, 'auth_url', None):\n            self.auth_url = self.api_url\n\n        # Determine the appropriate response parser using `self.api_format`\n        api_format = getattr(self, 'api_format', None)\n        if api_format == 'json':\n            self.api_parser = lambda r: r.json()\n        elif api_format == 'xml':\n            self.api_parser = lambda r: ET.fromstring(r.content)\n        elif api_format:\n            raise ValueError('unknown API format: '+str(api_format))\n\n        # Make sure the subclass was implemented properly.\n        missing_attrs = [a for a in self.required_attrs if not hasattr(self, a)]\n        if missing_attrs:\n            msg = \"The class %s is missing these required attributes: %s\"\n            msg %= self.__class__.__name__, ', '.join(missing_attrs)\n            raise AttributeError(msg)\n\n    def api_get(self, path, sess=None, **kw):\n        \"\"\"\n        Given a `path` (e.g. /users/foo), this function sends a GET request to\n        the platform's API (e.g. https://api.github.com/users/foo).\n\n        The response is returned, after checking its status code and ratelimit\n        headers.\n        \"\"\"\n        if not sess:\n            sess = self.get_auth_session()\n        response = sess.get(self.api_url+path, **kw)\n\n        # Check status\n        status = response.status_code\n        if status == 404:\n            raise Response(404, response.text)\n        elif status != 200:\n            log('{} api responded with {}:\\n{}'.format(self.name, status, response.text)\n               , level=logging.ERROR)\n            raise Response(500, '{} lookup failed with {}'.format(self.name, status))\n\n        # Check ratelimit headers\n        prefix = getattr(self, 'ratelimit_headers_prefix', None)\n        if prefix:\n            limit = response.headers[prefix+'limit']\n            remaining = response.headers[prefix+'remaining']\n            reset = response.headers[prefix+'reset']\n            try:\n                limit, remaining, reset = int(limit), int(remaining), int(reset)\n            except (TypeError, ValueError):\n                d = dict(limit=limit, remaining=remaining, reset=reset)\n                log('Got weird rate headers from %s: %s' % (self.name, d))\n            else:\n                percent_remaining = remaining/limit\n                if percent_remaining < 0.5:\n                    reset = to_age(datetime.fromtimestamp(reset, tz=utc))\n                    log_msg = (\n                        '{0} API: {1:.1%} of ratelimit has been consumed, '\n                        '{2} requests remaining, resets {3}.'\n                    ).format(self.name, 1 - percent_remaining, remaining, reset)\n                    log_lvl = logging.WARNING\n                    if percent_remaining < 0.2:\n                        log_lvl = logging.ERROR\n                    elif percent_remaining < 0.05:\n                        log_lvl = logging.CRITICAL\n                    log(log_msg, log_lvl)\n\n        return response\n\n    def extract_user_info(self, info):\n        \"\"\"\n        Given a user_info object of variable type (depending on the platform),\n        extract the relevant information by calling the platform's extractors\n        (`x_user_name`, `x_user_id`, etc).\n\n        Returns a `UserInfo`. The `user_id` attribute is guaranteed to have a\n        unique non-empty value.\n        \"\"\"\n        r = UserInfo(platform=self.name)\n        info = self.x_user_info(r, info, info)\n        r.user_name = self.x_user_name(r, info, None)\n        if self.x_user_id.__func__ is not_available:\n            r.user_id = r.user_name\n        else:\n            r.user_id = self.x_user_id(r, info)\n        assert r.user_id is not None\n        r.user_id = unicode(r.user_id)\n        assert len(r.user_id) > 0\n        r.display_name = self.x_display_name(r, info, None)\n        r.email = self.x_email(r, info, None)\n        r.avatar_url = self.x_avatar_url(r, info, None)\n        if not r.avatar_url:\n            gravatar_id = self.x_gravatar_id(r, info, None)\n            if r.email and not gravatar_id:\n                gravatar_id = hashlib.md5(r.email.strip().lower()).hexdigest()\n            if gravatar_id:\n                r.avatar_url = 'https://secure.gravatar.com/avatar/'+gravatar_id\n        r.is_team = self.x_is_team(r, info, False)\n        r.extra_info = info\n        return r\n\n    def get_team_members(self, team_name, page_url=None):\n        \"\"\"Given a team_name on the platform, return the team's membership list\n        from the API.\n        \"\"\"\n        default_url = self.api_team_members_path.format(user_name=quote(team_name))\n        r = self.api_get(page_url or default_url)\n        members, count, pages_urls = self.api_paginator(r, self.api_parser(r))\n        members = [self.extract_user_info(m) for m in members]\n        return members, count, pages_urls\n\n    def get_user_info(self, user_name, sess=None):\n        \"\"\"Given a user_name on the platform, get the user's info from the API.\n        \"\"\"\n        try:\n            path = self.api_user_info_path.format(user_name=quote(user_name))\n        except KeyError:\n            raise Response(404)\n        info = self.api_parser(self.api_get(path, sess=sess))\n        return self.extract_user_info(info)\n\n    def get_user_self_info(self, sess):\n        \"\"\"Get the authenticated user's info from the API.\n        \"\"\"\n        r = self.api_get(self.api_user_self_info_path, sess=sess)\n        info = self.extract_user_info(self.api_parser(r))\n        token = getattr(sess, 'token', None)\n        if token:\n            info.token = json.dumps(token)\n        return info\n\n\nclass PlatformOAuth1(Platform):\n\n    request_token_path = '/oauth/request_token'\n    authorize_path = '/oauth/authorize'\n    access_token_path = '/oauth/access_token'\n\n    def get_auth_session(self, token=None):\n        args = ()\n        if token:\n            args = (token['token'], token['token_secret'])\n        return OAuth1Session(self.api_key, self.api_secret, *args,\n                             callback_uri=self.callback_url)\n\n    def get_auth_url(self, **kw):\n        sess = self.get_auth_session()\n        r = sess.fetch_request_token(self.auth_url+self.request_token_path)\n        url = sess.authorization_url(self.auth_url+self.authorize_path)\n        return url, r['oauth_token'], r['oauth_token_secret']\n\n    def get_query_id(self, querystring):\n        return querystring['oauth_token']\n\n    def handle_auth_callback(self, url, token, token_secret):\n        sess = self.get_auth_session(dict(token=token, token_secret=token_secret))\n        sess.parse_authorization_response(url)\n        r = sess.fetch_access_token(self.auth_url+self.access_token_path)\n        sess.token = dict(token=r['oauth_token'],\n                          token_secret=r['oauth_token_secret'])\n        return sess\n\n\nclass PlatformOAuth2(Platform):\n\n    oauth_default_scope = None\n    oauth_email_scope = None\n    oauth_payment_scope = None\n\n    def get_auth_session(self, state=None, token=None, token_updater=None):\n        return OAuth2Session(self.api_key, state=state, token=token,\n                             token_updater=token_updater,\n                             redirect_uri=self.callback_url,\n                             scope=self.oauth_default_scope)\n\n    def get_auth_url(self, **kw):\n        sess = self.get_auth_session()\n        url, state = sess.authorization_url(self.auth_url)\n        return url, state, ''\n\n    def get_query_id(self, querystring):\n        return querystring['state']\n\n    def handle_auth_callback(self, url, state, unused_arg):\n        sess = self.get_auth_session(state=state)\n        sess.fetch_token(self.access_token_url,\n                         client_secret=self.api_secret,\n                         authorization_response=url)\n        return sess\n", "path": "gratipay/elsewhere/__init__.py"}, {"content": "from __future__ import absolute_import, division, print_function, unicode_literals\n\nfrom aspen import Response\nfrom gratipay.elsewhere import PlatformOAuth1\nfrom gratipay.elsewhere._extractors import any_key, key, not_available\nfrom gratipay.elsewhere._paginators import keys_paginator\n\n\nclass Bitbucket(PlatformOAuth1):\n\n    # Platform attributes\n    name = 'bitbucket'\n    display_name = 'Bitbucket'\n    account_url = 'https://bitbucket.org/{user_name}'\n\n    # Auth attributes\n    auth_url = 'https://bitbucket.org/api/1.0'\n    authorize_path = '/oauth/authenticate'\n\n    # API attributes\n    api_format = 'json'\n    api_paginator = keys_paginator(prev='previous')\n    api_url = 'https://bitbucket.org/api'\n    api_user_info_path = '/2.0/users/{user_name}'\n    api_user_self_info_path = '/2.0/user'\n    api_team_members_path = '/2.0/teams/{user_name}/members'\n\n    # User info extractors\n    x_user_info = key('user')\n    x_user_id = key('uuid')\n    x_user_name = key('username')\n    x_display_name = key('display_name')\n    x_email = not_available\n    x_avatar_url = any_key('avatar', ('links', 'avatar', 'href'))\n    x_is_team = key('type', lambda v: v == 'team')\n\n    def api_get(self, path, sess=None, **kw):\n        \"\"\"Extend to manually retry /users/pypy as /teams/pypy.\n\n        Bitbucket gives us a 404 where a 30x would be more helpful.\n\n        \"\"\"\n        try:\n            return PlatformOAuth1.api_get(self, path, sess, **kw)\n        except Response, response:\n            if response.code == 404 and ' is a team account' in response.body:\n                assert path.startswith('/2.0/users/')\n                path = '/2.0/teams/' + path[11:]\n                return PlatformOAuth1.api_get(self, path, sess, **kw)\n            else:\n                raise\n", "path": "gratipay/elsewhere/bitbucket.py"}]}