Skip to content

Commit e6c476e

Browse files
Merge pull request #290 from linode/dev
Release v5.5.0
2 parents dc592c7 + 2142e4d commit e6c476e

27 files changed

+417
-627
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ docs/_build/*
1010
.pytest_cache/*
1111
.tox/*
1212
venv
13-
baked_version
13+
baked_version
14+
.vscode

linode_api4/groups/database.py

-65
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
Database,
66
DatabaseEngine,
77
DatabaseType,
8-
MongoDBDatabase,
98
MySQLDatabase,
109
PostgreSQLDatabase,
1110
)
@@ -200,67 +199,3 @@ def postgresql_create(self, label, region, engine, ltype, **kwargs):
200199

201200
d = PostgreSQLDatabase(self.client, result["id"], result)
202201
return d
203-
204-
def mongodb_instances(self, *filters):
205-
"""
206-
Returns a list of Managed MongoDB Databases active on this account.
207-
208-
API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-databases-list
209-
210-
:param filters: Any number of filters to apply to this query.
211-
212-
:returns: A list of MongoDB databases that matched the query.
213-
:rtype: PaginatedList of MongoDBDatabase
214-
"""
215-
return self.client._get_and_filter(MongoDBDatabase, *filters)
216-
217-
def mongodb_create(self, label, region, engine, ltype, **kwargs):
218-
"""
219-
Creates an :any:`MongoDBDatabase` on this account with
220-
the given label, region, engine, and node type. For example::
221-
222-
client = LinodeClient(TOKEN)
223-
224-
# look up Region and Types to use. In this example I'm just using
225-
# the first ones returned.
226-
region = client.regions().first()
227-
node_type = client.database.types()[0]
228-
engine = client.database.engines(DatabaseEngine.engine == 'mongodb')[0]
229-
230-
new_database = client.database.mongodb_create(
231-
"example-database",
232-
region,
233-
engine.id,
234-
type.id
235-
)
236-
237-
API Documentation: https://www.linode.com/docs/api/databases/#managed-mongodb-database-create
238-
239-
:param label: The name for this cluster
240-
:type label: str
241-
:param region: The region to deploy this cluster in
242-
:type region: str or Region
243-
:param engine: The engine to deploy this cluster with
244-
:type engine: str or Engine
245-
:param ltype: The Linode Type to use for this cluster
246-
:type ltype: str or Type
247-
"""
248-
249-
params = {
250-
"label": label,
251-
"region": region.id if issubclass(type(region), Base) else region,
252-
"engine": engine.id if issubclass(type(engine), Base) else engine,
253-
"type": ltype.id if issubclass(type(ltype), Base) else ltype,
254-
}
255-
params.update(kwargs)
256-
257-
result = self.client.post("/databases/mongodb/instances", data=params)
258-
259-
if "id" not in result:
260-
raise UnexpectedResponseError(
261-
"Unexpected response when creating MongoDB Database",
262-
json=result,
263-
)
264-
265-
d = MongoDBDatabase(self.client, result["id"], result)
266-
return d

linode_api4/linode_client.py

+87-38
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import json
44
import logging
5-
import time
65
from typing import BinaryIO, Tuple
76

87
import pkg_resources
98
import requests
9+
from requests.adapters import HTTPAdapter, Retry
1010

1111
from linode_api4.errors import ApiError, UnexpectedResponseError
1212
from linode_api4.groups import *
@@ -22,14 +22,27 @@
2222
logger = logging.getLogger(__name__)
2323

2424

25+
class LinearRetry(Retry):
26+
"""
27+
Linear retry is a subclass of Retry that uses a linear backoff strategy.
28+
This is necessary to maintain backwards compatibility with the old retry system.
29+
"""
30+
31+
def get_backoff_time(self):
32+
return self.backoff_factor
33+
34+
2535
class LinodeClient:
2636
def __init__(
2737
self,
2838
token,
2939
base_url="https://api.linode.com/v4",
3040
user_agent=None,
3141
page_size=None,
32-
retry_rate_limit_interval=None,
42+
retry=True,
43+
retry_rate_limit_interval=1.0,
44+
retry_max=5,
45+
retry_statuses=None,
3346
):
3447
"""
3548
The main interface to the Linode API.
@@ -51,26 +64,57 @@ def __init__(
5164
can be found in the API docs, but at time of writing
5265
are between 25 and 500.
5366
:type page_size: int
54-
:param retry_rate_limit_interval: If given, 429 responses will be automatically
55-
retried up to 5 times with the given interval,
56-
in seconds, between attempts.
57-
:type retry_rate_limit_interval: int
67+
:param retry: Whether API requests should automatically be retries on known
68+
intermittent responses.
69+
:type retry: bool
70+
:param retry_rate_limit_interval: The amount of time to wait between HTTP request
71+
retries.
72+
:type retry_rate_limit_interval: float
73+
:param retry_max: The number of request retries that should be attempted before
74+
raising an API error.
75+
:type retry_max: int
76+
:type retry_statuses: List of int
77+
:param retry_statuses: Additional HTTP response statuses to retry on.
78+
By default, the client will retry on 408, 429, and 502
79+
responses.
5880
"""
5981
self.base_url = base_url
6082
self._add_user_agent = user_agent
6183
self.token = token
62-
self.session = requests.Session()
6384
self.page_size = page_size
64-
self.retry_rate_limit_interval = retry_rate_limit_interval
85+
86+
retry_forcelist = [408, 429, 502]
87+
88+
if retry_statuses is not None:
89+
retry_forcelist.extend(retry_statuses)
6590

6691
# make sure we got a sane backoff
67-
if self.retry_rate_limit_interval is not None:
68-
if not isinstance(self.retry_rate_limit_interval, int):
69-
raise ValueError("retry_rate_limit_interval must be an int")
70-
if self.retry_rate_limit_interval < 1:
71-
raise ValueError(
72-
"retry_rate_limit_interval must not be less than 1"
73-
)
92+
if not isinstance(retry_rate_limit_interval, float):
93+
raise ValueError("retry_rate_limit_interval must be a float")
94+
95+
# Ensure the max retries value is valid
96+
if not isinstance(retry_max, int):
97+
raise ValueError("retry_max must be an int")
98+
99+
self.retry = retry
100+
self.retry_rate_limit_interval = retry_rate_limit_interval
101+
self.retry_max = retry_max
102+
self.retry_statuses = retry_statuses
103+
104+
# Initialize the HTTP client session
105+
self.session = requests.Session()
106+
107+
self._retry_config = LinearRetry(
108+
total=retry_max if retry else 0,
109+
status_forcelist=retry_forcelist,
110+
respect_retry_after_header=True,
111+
backoff_factor=retry_rate_limit_interval,
112+
raise_on_status=False,
113+
)
114+
retry_adapter = HTTPAdapter(max_retries=self._retry_config)
115+
116+
self.session.mount("http://", retry_adapter)
117+
self.session.mount("https://", retry_adapter)
74118

75119
#: Access methods related to Linodes - see :any:`LinodeGroup` for
76120
#: more information
@@ -196,29 +240,11 @@ def _api_call(
196240
if data is not None:
197241
body = json.dumps(data)
198242

199-
# retry on 429 response
200-
max_retries = 5 if self.retry_rate_limit_interval else 1
201-
for attempt in range(max_retries):
202-
response = method(url, headers=headers, data=body)
203-
204-
warning = response.headers.get("Warning", None)
205-
if warning:
206-
logger.warning(
207-
"Received warning from server: {}".format(warning)
208-
)
209-
210-
# if we were configured to retry 429s, and we got a 429, sleep briefly and then retry
211-
if self.retry_rate_limit_interval and response.status_code == 429:
212-
logger.warning(
213-
"Received 429 response; waiting {} seconds and retrying request (attempt {}/{})".format(
214-
self.retry_rate_limit_interval,
215-
attempt,
216-
max_retries,
217-
)
218-
)
219-
time.sleep(self.retry_rate_limit_interval)
220-
else:
221-
break
243+
response = method(url, headers=headers, data=body)
244+
245+
warning = response.headers.get("Warning", None)
246+
if warning:
247+
logger.warning("Received warning from server: {}".format(warning))
222248

223249
if 399 < response.status_code < 600:
224250
j = None
@@ -288,6 +314,29 @@ def put(self, *args, **kwargs):
288314
def delete(self, *args, **kwargs):
289315
return self._api_call(*args, method=self.session.delete, **kwargs)
290316

317+
def __setattr__(self, key, value):
318+
# Allow for dynamic updating of the retry config
319+
handlers = {
320+
"retry_rate_limit_interval": lambda: setattr(
321+
self._retry_config, "backoff_factor", value
322+
),
323+
"retry": lambda: setattr(
324+
self._retry_config, "total", self.retry_max if value else 0
325+
),
326+
"retry_max": lambda: setattr(
327+
self._retry_config, "total", value if self.retry else 0
328+
),
329+
"retry_statuses": lambda: setattr(
330+
self._retry_config, "status_forcelist", value
331+
),
332+
}
333+
334+
handler = handlers.get(key)
335+
if hasattr(self, "_retry_config") and handler is not None:
336+
handler()
337+
338+
super().__setattr__(key, value)
339+
291340
def image_create(self, disk, label=None, description=None):
292341
"""
293342
.. note:: This method is an alias to maintain backwards compatibility.

linode_api4/objects/base.py

+32-4
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,18 @@ def __repr__(self):
9090

9191
@property
9292
def dict(self):
93-
return dict(self.__dict__)
93+
result = vars(self).copy()
94+
cls = type(self)
95+
96+
for k, v in result.items():
97+
if isinstance(v, cls):
98+
result[k] = v.dict
99+
elif isinstance(v, list):
100+
result[k] = [
101+
item.dict if isinstance(item, cls) else item for item in v
102+
]
103+
104+
return result
94105

95106

96107
class Base(object, metaclass=FilterableMetaclass):
@@ -210,9 +221,26 @@ def save(self, force=True) -> bool:
210221
if not force and not self._changed:
211222
return False
212223

213-
resp = self._client.put(
214-
type(self).api_endpoint, model=self, data=self._serialize()
215-
)
224+
data = None
225+
if not self._populated:
226+
data = {
227+
a: object.__getattribute__(self, a)
228+
for a in type(self).properties
229+
if type(self).properties[a].mutable
230+
and object.__getattribute__(self, a) is not None
231+
}
232+
233+
for key, value in data.items():
234+
if (
235+
isinstance(value, ExplicitNullValue)
236+
or value == ExplicitNullValue
237+
):
238+
data[key] = None
239+
240+
else:
241+
data = self._serialize()
242+
243+
resp = self._client.put(type(self).api_endpoint, model=self, data=data)
216244

217245
if "error" in resp:
218246
return False

0 commit comments

Comments
 (0)