We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
v2 API Retry on 429 status_code if users are throttled?
Right now we [Only retry on request errors with 5XX status code and use exponential backoff](https://github.com/plotly/plotly.py/pull/1146). But should we also retry for 429 status codes which users receive if they go over Plot.ly's throttling limit? Especially since calls to the image servers are now limited to 1/sec.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `plotly/api/v2/utils.py`
Content:
```
1 from __future__ import absolute_import
2 
3 import requests
4 from requests.compat import json as _json
5 from requests.exceptions import RequestException
6 from retrying import retry
7 from plotly import config, exceptions, version, utils
8 from plotly.api.utils import basic_auth
9 
10 
11 def make_params(**kwargs):
12     """
13     Helper to create a params dict, skipping undefined entries.
14 
15     :returns: (dict) A params dict to pass to `request`.
16 
17     """
18     return {k: v for k, v in kwargs.items() if v is not None}
19 
20 
21 def build_url(resource, id='', route=''):
22     """
23     Create a url for a request on a V2 resource.
24 
25     :param (str) resource: E.g., 'files', 'plots', 'grids', etc.
26     :param (str) id: The unique identifier for the resource.
27     :param (str) route: Detail/list route. E.g., 'restore', 'lookup', etc.
28     :return: (str) The url.
29 
30     """
31     base = config.get_config()['plotly_api_domain']
32     formatter = {'base': base, 'resource': resource, 'id': id, 'route': route}
33 
34     # Add path to base url depending on the input params. Note that `route`
35     # can refer to a 'list' or a 'detail' route. Since it cannot refer to
36     # both at the same time, it's overloaded in this function.
37     if id:
38         if route:
39             url = '{base}/v2/{resource}/{id}/{route}'.format(**formatter)
40         else:
41             url = '{base}/v2/{resource}/{id}'.format(**formatter)
42     else:
43         if route:
44             url = '{base}/v2/{resource}/{route}'.format(**formatter)
45         else:
46             url = '{base}/v2/{resource}'.format(**formatter)
47 
48     return url
49 
50 
51 def validate_response(response):
52     """
53     Raise a helpful PlotlyRequestError for failed requests.
54 
55     :param (requests.Response) response: A Response object from an api request.
56     :raises: (PlotlyRequestError) If the request failed for any reason.
57     :returns: (None)
58 
59     """
60     if response.ok:
61         return
62 
63     content = response.content
64     status_code = response.status_code
65     try:
66         parsed_content = response.json()
67     except ValueError:
68         message = content if content else 'No Content'
69         raise exceptions.PlotlyRequestError(message, status_code, content)
70 
71     message = ''
72     if isinstance(parsed_content, dict):
73         errors = parsed_content.get('errors', [])
74         messages = [error.get('message') for error in errors]
75         message = '\n'.join([msg for msg in messages if msg])
76     if not message:
77         message = content if content else 'No Content'
78 
79     raise exceptions.PlotlyRequestError(message, status_code, content)
80 
81 
82 def get_headers():
83     """
84     Using session credentials/config, get headers for a V2 API request.
85 
86     Users may have their own proxy layer and so we free up the `authorization`
87     header for this purpose (instead adding the user authorization in a new
88     `plotly-authorization` header). See pull #239.
89 
90     :returns: (dict) Headers to add to a requests.request call.
91 
92     """
93     creds = config.get_credentials()
94 
95     headers = {
96         'plotly-client-platform': 'python {}'.format(version.stable_semver()),
97         'content-type': 'application/json'
98     }
99 
100     plotly_auth = basic_auth(creds['username'], creds['api_key'])
101     proxy_auth = basic_auth(creds['proxy_username'], creds['proxy_password'])
102 
103     if config.get_config()['plotly_proxy_authorization']:
104         headers['authorization'] = proxy_auth
105         if creds['username'] and creds['api_key']:
106             headers['plotly-authorization'] = plotly_auth
107     else:
108         if creds['username'] and creds['api_key']:
109             headers['authorization'] = plotly_auth
110 
111     return headers
112 
113 
114 def should_retry(exception):
115     if isinstance(exception, exceptions.PlotlyRequestError):
116         if (isinstance(exception.status_code, int) and
117                 500 <= exception.status_code < 600):
118             # Retry on 5XX errors.
119             return True
120         elif 'Uh oh, an error occurred' in exception.message:
121             return True
122 
123     return False
124 
125 
126 @retry(wait_exponential_multiplier=1000, wait_exponential_max=16000,
127        stop_max_delay=180000, retry_on_exception=should_retry)
128 def request(method, url, **kwargs):
129     """
130     Central place to make any api v2 api request.
131 
132     :param (str) method: The request method ('get', 'put', 'delete', ...).
133     :param (str) url: The full api url to make the request to.
134     :param kwargs: These are passed along (but possibly mutated) to requests.
135     :return: (requests.Response) The response directly from requests.
136 
137     """
138     kwargs['headers'] = dict(kwargs.get('headers', {}), **get_headers())
139 
140     # Change boolean params to lowercase strings. E.g., `True` --> `'true'`.
141     # Just change the value so that requests handles query string creation.
142     if isinstance(kwargs.get('params'), dict):
143         kwargs['params'] = kwargs['params'].copy()
144         for key in kwargs['params']:
145             if isinstance(kwargs['params'][key], bool):
146                 kwargs['params'][key] = _json.dumps(kwargs['params'][key])
147 
148     # We have a special json encoding class for non-native objects.
149     if kwargs.get('json') is not None:
150         if kwargs.get('data'):
151             raise exceptions.PlotlyError('Cannot supply data and json kwargs.')
152         kwargs['data'] = _json.dumps(kwargs.pop('json'), sort_keys=True,
153                                      cls=utils.PlotlyJSONEncoder)
154 
155     # The config file determines whether reuqests should *verify*.
156     kwargs['verify'] = config.get_config()['plotly_ssl_verification']
157 
158     try:
159         response = requests.request(method, url, **kwargs)
160     except RequestException as e:
161         # The message can be an exception. E.g., MaxRetryError.
162         message = str(getattr(e, 'message', 'No message'))
163         response = getattr(e, 'response', None)
164         status_code = response.status_code if response else None
165         content = response.content if response else 'No content'
166         raise exceptions.PlotlyRequestError(message, status_code, content)
167     validate_response(response)
168     return response
169
```
--- 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/plotly/api/v2/utils.py b/plotly/api/v2/utils.py
--- a/plotly/api/v2/utils.py
+++ b/plotly/api/v2/utils.py
@@ -114,8 +114,8 @@
 def should_retry(exception):
     if isinstance(exception, exceptions.PlotlyRequestError):
         if (isinstance(exception.status_code, int) and
-                500 <= exception.status_code < 600):
-            # Retry on 5XX errors.
+                (500 <= exception.status_code < 600 or exception.status_code == 429)):
+            # Retry on 5XX and 429 (image export throttling) errors.
             return True
         elif 'Uh oh, an error occurred' in exception.message:
             return True

{"golden_diff": "diff --git a/plotly/api/v2/utils.py b/plotly/api/v2/utils.py\n--- a/plotly/api/v2/utils.py\n+++ b/plotly/api/v2/utils.py\n@@ -114,8 +114,8 @@\n def should_retry(exception):\n     if isinstance(exception, exceptions.PlotlyRequestError):\n         if (isinstance(exception.status_code, int) and\n-                500 <= exception.status_code < 600):\n-            # Retry on 5XX errors.\n+                (500 <= exception.status_code < 600 or exception.status_code == 429)):\n+            # Retry on 5XX and 429 (image export throttling) errors.\n             return True\n         elif 'Uh oh, an error occurred' in exception.message:\n             return True\n", "issue": "v2 API Retry on 429 status_code if users are throttled?\nRight now we [Only retry on request errors with 5XX status code and use exponential backoff](https://github.com/plotly/plotly.py/pull/1146). But should we also retry for 429 status codes which users receive if they go over Plot.ly's throttling limit? Especially since calls to the image servers are now limited to 1/sec.\n", "before_files": [{"content": "from __future__ import absolute_import\n\nimport requests\nfrom requests.compat import json as _json\nfrom requests.exceptions import RequestException\nfrom retrying import retry\nfrom plotly import config, exceptions, version, utils\nfrom plotly.api.utils import basic_auth\n\n\ndef make_params(**kwargs):\n    \"\"\"\n    Helper to create a params dict, skipping undefined entries.\n\n    :returns: (dict) A params dict to pass to `request`.\n\n    \"\"\"\n    return {k: v for k, v in kwargs.items() if v is not None}\n\n\ndef build_url(resource, id='', route=''):\n    \"\"\"\n    Create a url for a request on a V2 resource.\n\n    :param (str) resource: E.g., 'files', 'plots', 'grids', etc.\n    :param (str) id: The unique identifier for the resource.\n    :param (str) route: Detail/list route. E.g., 'restore', 'lookup', etc.\n    :return: (str) The url.\n\n    \"\"\"\n    base = config.get_config()['plotly_api_domain']\n    formatter = {'base': base, 'resource': resource, 'id': id, 'route': route}\n\n    # Add path to base url depending on the input params. Note that `route`\n    # can refer to a 'list' or a 'detail' route. Since it cannot refer to\n    # both at the same time, it's overloaded in this function.\n    if id:\n        if route:\n            url = '{base}/v2/{resource}/{id}/{route}'.format(**formatter)\n        else:\n            url = '{base}/v2/{resource}/{id}'.format(**formatter)\n    else:\n        if route:\n            url = '{base}/v2/{resource}/{route}'.format(**formatter)\n        else:\n            url = '{base}/v2/{resource}'.format(**formatter)\n\n    return url\n\n\ndef validate_response(response):\n    \"\"\"\n    Raise a helpful PlotlyRequestError for failed requests.\n\n    :param (requests.Response) response: A Response object from an api request.\n    :raises: (PlotlyRequestError) If the request failed for any reason.\n    :returns: (None)\n\n    \"\"\"\n    if response.ok:\n        return\n\n    content = response.content\n    status_code = response.status_code\n    try:\n        parsed_content = response.json()\n    except ValueError:\n        message = content if content else 'No Content'\n        raise exceptions.PlotlyRequestError(message, status_code, content)\n\n    message = ''\n    if isinstance(parsed_content, dict):\n        errors = parsed_content.get('errors', [])\n        messages = [error.get('message') for error in errors]\n        message = '\\n'.join([msg for msg in messages if msg])\n    if not message:\n        message = content if content else 'No Content'\n\n    raise exceptions.PlotlyRequestError(message, status_code, content)\n\n\ndef get_headers():\n    \"\"\"\n    Using session credentials/config, get headers for a V2 API request.\n\n    Users may have their own proxy layer and so we free up the `authorization`\n    header for this purpose (instead adding the user authorization in a new\n    `plotly-authorization` header). See pull #239.\n\n    :returns: (dict) Headers to add to a requests.request call.\n\n    \"\"\"\n    creds = config.get_credentials()\n\n    headers = {\n        'plotly-client-platform': 'python {}'.format(version.stable_semver()),\n        'content-type': 'application/json'\n    }\n\n    plotly_auth = basic_auth(creds['username'], creds['api_key'])\n    proxy_auth = basic_auth(creds['proxy_username'], creds['proxy_password'])\n\n    if config.get_config()['plotly_proxy_authorization']:\n        headers['authorization'] = proxy_auth\n        if creds['username'] and creds['api_key']:\n            headers['plotly-authorization'] = plotly_auth\n    else:\n        if creds['username'] and creds['api_key']:\n            headers['authorization'] = plotly_auth\n\n    return headers\n\n\ndef should_retry(exception):\n    if isinstance(exception, exceptions.PlotlyRequestError):\n        if (isinstance(exception.status_code, int) and\n                500 <= exception.status_code < 600):\n            # Retry on 5XX errors.\n            return True\n        elif 'Uh oh, an error occurred' in exception.message:\n            return True\n\n    return False\n\n\n@retry(wait_exponential_multiplier=1000, wait_exponential_max=16000,\n       stop_max_delay=180000, retry_on_exception=should_retry)\ndef request(method, url, **kwargs):\n    \"\"\"\n    Central place to make any api v2 api request.\n\n    :param (str) method: The request method ('get', 'put', 'delete', ...).\n    :param (str) url: The full api url to make the request to.\n    :param kwargs: These are passed along (but possibly mutated) to requests.\n    :return: (requests.Response) The response directly from requests.\n\n    \"\"\"\n    kwargs['headers'] = dict(kwargs.get('headers', {}), **get_headers())\n\n    # Change boolean params to lowercase strings. E.g., `True` --> `'true'`.\n    # Just change the value so that requests handles query string creation.\n    if isinstance(kwargs.get('params'), dict):\n        kwargs['params'] = kwargs['params'].copy()\n        for key in kwargs['params']:\n            if isinstance(kwargs['params'][key], bool):\n                kwargs['params'][key] = _json.dumps(kwargs['params'][key])\n\n    # We have a special json encoding class for non-native objects.\n    if kwargs.get('json') is not None:\n        if kwargs.get('data'):\n            raise exceptions.PlotlyError('Cannot supply data and json kwargs.')\n        kwargs['data'] = _json.dumps(kwargs.pop('json'), sort_keys=True,\n                                     cls=utils.PlotlyJSONEncoder)\n\n    # The config file determines whether reuqests should *verify*.\n    kwargs['verify'] = config.get_config()['plotly_ssl_verification']\n\n    try:\n        response = requests.request(method, url, **kwargs)\n    except RequestException as e:\n        # The message can be an exception. E.g., MaxRetryError.\n        message = str(getattr(e, 'message', 'No message'))\n        response = getattr(e, 'response', None)\n        status_code = response.status_code if response else None\n        content = response.content if response else 'No content'\n        raise exceptions.PlotlyRequestError(message, status_code, content)\n    validate_response(response)\n    return response\n", "path": "plotly/api/v2/utils.py"}], "after_files": [{"content": "from __future__ import absolute_import\n\nimport requests\nfrom requests.compat import json as _json\nfrom requests.exceptions import RequestException\nfrom retrying import retry\nfrom plotly import config, exceptions, version, utils\nfrom plotly.api.utils import basic_auth\n\n\ndef make_params(**kwargs):\n    \"\"\"\n    Helper to create a params dict, skipping undefined entries.\n\n    :returns: (dict) A params dict to pass to `request`.\n\n    \"\"\"\n    return {k: v for k, v in kwargs.items() if v is not None}\n\n\ndef build_url(resource, id='', route=''):\n    \"\"\"\n    Create a url for a request on a V2 resource.\n\n    :param (str) resource: E.g., 'files', 'plots', 'grids', etc.\n    :param (str) id: The unique identifier for the resource.\n    :param (str) route: Detail/list route. E.g., 'restore', 'lookup', etc.\n    :return: (str) The url.\n\n    \"\"\"\n    base = config.get_config()['plotly_api_domain']\n    formatter = {'base': base, 'resource': resource, 'id': id, 'route': route}\n\n    # Add path to base url depending on the input params. Note that `route`\n    # can refer to a 'list' or a 'detail' route. Since it cannot refer to\n    # both at the same time, it's overloaded in this function.\n    if id:\n        if route:\n            url = '{base}/v2/{resource}/{id}/{route}'.format(**formatter)\n        else:\n            url = '{base}/v2/{resource}/{id}'.format(**formatter)\n    else:\n        if route:\n            url = '{base}/v2/{resource}/{route}'.format(**formatter)\n        else:\n            url = '{base}/v2/{resource}'.format(**formatter)\n\n    return url\n\n\ndef validate_response(response):\n    \"\"\"\n    Raise a helpful PlotlyRequestError for failed requests.\n\n    :param (requests.Response) response: A Response object from an api request.\n    :raises: (PlotlyRequestError) If the request failed for any reason.\n    :returns: (None)\n\n    \"\"\"\n    if response.ok:\n        return\n\n    content = response.content\n    status_code = response.status_code\n    try:\n        parsed_content = response.json()\n    except ValueError:\n        message = content if content else 'No Content'\n        raise exceptions.PlotlyRequestError(message, status_code, content)\n\n    message = ''\n    if isinstance(parsed_content, dict):\n        errors = parsed_content.get('errors', [])\n        messages = [error.get('message') for error in errors]\n        message = '\\n'.join([msg for msg in messages if msg])\n    if not message:\n        message = content if content else 'No Content'\n\n    raise exceptions.PlotlyRequestError(message, status_code, content)\n\n\ndef get_headers():\n    \"\"\"\n    Using session credentials/config, get headers for a V2 API request.\n\n    Users may have their own proxy layer and so we free up the `authorization`\n    header for this purpose (instead adding the user authorization in a new\n    `plotly-authorization` header). See pull #239.\n\n    :returns: (dict) Headers to add to a requests.request call.\n\n    \"\"\"\n    creds = config.get_credentials()\n\n    headers = {\n        'plotly-client-platform': 'python {}'.format(version.stable_semver()),\n        'content-type': 'application/json'\n    }\n\n    plotly_auth = basic_auth(creds['username'], creds['api_key'])\n    proxy_auth = basic_auth(creds['proxy_username'], creds['proxy_password'])\n\n    if config.get_config()['plotly_proxy_authorization']:\n        headers['authorization'] = proxy_auth\n        if creds['username'] and creds['api_key']:\n            headers['plotly-authorization'] = plotly_auth\n    else:\n        if creds['username'] and creds['api_key']:\n            headers['authorization'] = plotly_auth\n\n    return headers\n\n\ndef should_retry(exception):\n    if isinstance(exception, exceptions.PlotlyRequestError):\n        if (isinstance(exception.status_code, int) and\n                (500 <= exception.status_code < 600 or exception.status_code == 429)):\n            # Retry on 5XX and 429 (image export throttling) errors.\n            return True\n        elif 'Uh oh, an error occurred' in exception.message:\n            return True\n\n    return False\n\n\n@retry(wait_exponential_multiplier=1000, wait_exponential_max=16000,\n       stop_max_delay=180000, retry_on_exception=should_retry)\ndef request(method, url, **kwargs):\n    \"\"\"\n    Central place to make any api v2 api request.\n\n    :param (str) method: The request method ('get', 'put', 'delete', ...).\n    :param (str) url: The full api url to make the request to.\n    :param kwargs: These are passed along (but possibly mutated) to requests.\n    :return: (requests.Response) The response directly from requests.\n\n    \"\"\"\n    kwargs['headers'] = dict(kwargs.get('headers', {}), **get_headers())\n\n    # Change boolean params to lowercase strings. E.g., `True` --> `'true'`.\n    # Just change the value so that requests handles query string creation.\n    if isinstance(kwargs.get('params'), dict):\n        kwargs['params'] = kwargs['params'].copy()\n        for key in kwargs['params']:\n            if isinstance(kwargs['params'][key], bool):\n                kwargs['params'][key] = _json.dumps(kwargs['params'][key])\n\n    # We have a special json encoding class for non-native objects.\n    if kwargs.get('json') is not None:\n        if kwargs.get('data'):\n            raise exceptions.PlotlyError('Cannot supply data and json kwargs.')\n        kwargs['data'] = _json.dumps(kwargs.pop('json'), sort_keys=True,\n                                     cls=utils.PlotlyJSONEncoder)\n\n    # The config file determines whether reuqests should *verify*.\n    kwargs['verify'] = config.get_config()['plotly_ssl_verification']\n\n    try:\n        response = requests.request(method, url, **kwargs)\n    except RequestException as e:\n        # The message can be an exception. E.g., MaxRetryError.\n        message = str(getattr(e, 'message', 'No message'))\n        response = getattr(e, 'response', None)\n        status_code = response.status_code if response else None\n        content = response.content if response else 'No content'\n        raise exceptions.PlotlyRequestError(message, status_code, content)\n    validate_response(response)\n    return response\n", "path": "plotly/api/v2/utils.py"}]}