Skip to content

Commit 2fbed9e

Browse files
Merge pull request #677 from linode/dev
Release v5.42.0
2 parents d73884a + d692eef commit 2fbed9e

File tree

13 files changed

+221
-63
lines changed

13 files changed

+221
-63
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
runs-on: ubuntu-latest
3232
strategy:
3333
matrix:
34-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
34+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
3535
steps:
3636
- uses: actions/checkout@v6
3737
- uses: actions/setup-python@v6

.github/workflows/e2e-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ on:
5555
- dev
5656

5757
env:
58-
DEFAULT_PYTHON_VERSION: "3.10"
59-
EOL_PYTHON_VERSION: "3.9"
58+
DEFAULT_PYTHON_VERSION: "3.13"
59+
EOL_PYTHON_VERSION: "3.10"
6060
EXIT_STATUS: 0
6161

6262
jobs:

CODEOWNERS

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
* @linode/dx
2-
1+
* @linode/dx @linode/dx-sdets

linode_api4/objects/linode.py

Lines changed: 97 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import copy
24
import string
35
import sys
@@ -40,7 +42,11 @@
4042
from linode_api4.objects.serializable import JSONObject, StrEnum
4143
from linode_api4.objects.vpc import VPC, VPCSubnet
4244
from linode_api4.paginated_list import PaginatedList
43-
from linode_api4.util import drop_null_keys, generate_device_suffixes
45+
from linode_api4.util import (
46+
drop_null_keys,
47+
generate_device_suffixes,
48+
normalize_as_list,
49+
)
4450

4551
PASSWORD_CHARS = string.ascii_letters + string.digits + string.punctuation
4652
MIN_DEVICE_LIMIT = 8
@@ -1246,14 +1252,14 @@ def _func(value):
12461252
# create derived objects
12471253
def config_create(
12481254
self,
1249-
kernel=None,
1250-
label=None,
1251-
devices=[],
1252-
disks=[],
1253-
volumes=[],
1254-
interfaces=[],
1255+
kernel: Kernel | str | None = None,
1256+
label: str | None = None,
1257+
devices: "Disk | Volume | dict[str, Any] | list[Disk | Volume | dict[str, Any]] | None" = None,
1258+
disks: Disk | int | list[Disk | int] | None = None,
1259+
volumes: "Volume | int | list[Volume | int] | None" = None,
1260+
interfaces: list[ConfigInterface | dict[str, Any]] | None = None,
12551261
**kwargs,
1256-
):
1262+
) -> Config:
12571263
"""
12581264
Creates a Linode Config with the given attributes.
12591265
@@ -1263,17 +1269,22 @@ def config_create(
12631269
:param label: The config label
12641270
:param disks: The list of disks, starting at sda, to map to this config.
12651271
:param volumes: The volumes, starting after the last disk, to map to this
1266-
config
1272+
config.
12671273
:param devices: A list of devices to assign to this config, in device
1268-
index order. Values must be of type Disk or Volume. If this is
1269-
given, you may not include disks or volumes.
1274+
index order, a raw device mapping dict to pass directly to the API
1275+
(e.g. ``{"sda": {"disk_id": 123}, "sdb": Volume(...)}``), or
1276+
a single Disk or Volume.
1277+
If this is given, you may not include disks or volumes.
1278+
:param interfaces: A list of ConfigInterface objects or dicts to assign to this config.
12701279
:param **kwargs: Any other arguments accepted by the api.
12711280
12721281
:returns: A new Linode Config
12731282
"""
12741283
# needed here to avoid circular imports
12751284
from .volume import Volume # pylint: disable=import-outside-toplevel
12761285

1286+
interfaces = [] if interfaces is None else interfaces
1287+
12771288
hypervisor_prefix = "sd" if self.hypervisor == "kvm" else "xvd"
12781289

12791290
device_limit = int(
@@ -1288,52 +1299,83 @@ def config_create(
12881299
for suffix in generate_device_suffixes(device_limit)
12891300
]
12901301

1291-
device_map = {
1292-
device_names[i]: None for i in range(0, len(device_names))
1293-
}
1302+
def _flatten_device(device: Disk | Volume | dict | None):
1303+
if device is None:
1304+
return None
1305+
elif isinstance(device, Disk):
1306+
return {"disk_id": device.id}
1307+
elif isinstance(device, Volume):
1308+
return {"volume_id": device.id}
1309+
elif isinstance(device, dict):
1310+
return device
1311+
1312+
raise TypeError("Disk, Volume, or dict expected!")
1313+
1314+
def _device_entry(device: Disk | Volume | int, key: str):
1315+
if isinstance(device, (Disk, Volume)):
1316+
return _flatten_device(device)
1317+
1318+
try:
1319+
device_id = int(device)
1320+
except (TypeError, ValueError):
1321+
raise TypeError(
1322+
"Disk, Volume, or integer ID expected!"
1323+
) from None
1324+
1325+
return {key: device_id}
1326+
1327+
def _build_devices():
1328+
# Devices is a dict, flatten and pass through
1329+
if isinstance(devices, dict):
1330+
return {
1331+
k: (
1332+
_flatten_device(v)
1333+
if isinstance(v, (Disk, Volume))
1334+
else v
1335+
)
1336+
for k, v in devices.items()
1337+
}
12941338

1339+
device_list = []
1340+
1341+
if devices:
1342+
device_list += [
1343+
_flatten_device(device)
1344+
for device in normalize_as_list(devices)
1345+
]
1346+
1347+
if disks:
1348+
device_list += [
1349+
_device_entry(disk, "disk_id") if disk is not None else None
1350+
for disk in normalize_as_list(disks)
1351+
]
1352+
1353+
if volumes:
1354+
device_list += [
1355+
(
1356+
_device_entry(volume, "volume_id")
1357+
if volume is not None
1358+
else None
1359+
)
1360+
for volume in normalize_as_list(volumes)
1361+
]
1362+
1363+
return {
1364+
device_names[i]: device for i, device in enumerate(device_list)
1365+
}
1366+
1367+
# This validation is enforced for backwards compatibility but isn't
1368+
# technically needed anymore
12951369
if devices and (disks or volumes):
12961370
raise ValueError(
12971371
'You may not call config_create with "devices" and '
12981372
'either of "disks" or "volumes" specified!'
12991373
)
13001374

1301-
if not devices:
1302-
if not isinstance(disks, list):
1303-
disks = [disks]
1304-
if not isinstance(volumes, list):
1305-
volumes = [volumes]
1306-
1307-
devices = []
1308-
1309-
for d in disks:
1310-
if d is None:
1311-
devices.append(None)
1312-
elif isinstance(d, Disk):
1313-
devices.append(d)
1314-
else:
1315-
devices.append(Disk(self._client, int(d), self.id))
1316-
1317-
for v in volumes:
1318-
if v is None:
1319-
devices.append(None)
1320-
elif isinstance(v, Volume):
1321-
devices.append(v)
1322-
else:
1323-
devices.append(Volume(self._client, int(v)))
1324-
1325-
if not devices:
1326-
raise ValueError("Must include at least one disk or volume!")
1375+
device_map = _build_devices()
13271376

1328-
for i, d in enumerate(devices):
1329-
if d is None:
1330-
pass
1331-
elif isinstance(d, Disk):
1332-
device_map[device_names[i]] = {"disk_id": d.id}
1333-
elif isinstance(d, Volume):
1334-
device_map[device_names[i]] = {"volume_id": d.id}
1335-
else:
1336-
raise TypeError("Disk or Volume expected!")
1377+
if len(device_map) < 1:
1378+
raise ValueError("Must include at least one disk or volume!")
13371379

13381380
param_interfaces = []
13391381
for interface in interfaces:
@@ -1845,8 +1887,8 @@ def clone(
18451887
to_linode=None,
18461888
region=None,
18471889
instance_type=None,
1848-
configs=[],
1849-
disks=[],
1890+
configs=None,
1891+
disks=None,
18501892
label=None,
18511893
group=None,
18521894
with_backups=None,
@@ -1902,7 +1944,10 @@ def clone(
19021944
'You may only specify one of "to_linode" and "region"'
19031945
)
19041946

1905-
if region and not type:
1947+
configs = [] if configs is None else configs
1948+
disks = [] if disks is None else disks
1949+
1950+
if region and not instance_type:
19061951
raise ValueError('Specifying a region requires a "service" as well')
19071952

19081953
if not isinstance(configs, list) and not isinstance(

linode_api4/objects/lke.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Base,
99
DerivedBase,
1010
Instance,
11+
InstanceDiskEncryptionType,
1112
JSONObject,
1213
MappedObject,
1314
Property,
@@ -422,6 +423,9 @@ def node_pool_create(
422423
] = None,
423424
update_strategy: Optional[str] = None,
424425
label: str = None,
426+
disk_encryption: Optional[
427+
Union[str, InstanceDiskEncryptionType]
428+
] = None,
425429
**kwargs,
426430
):
427431
"""
@@ -443,6 +447,9 @@ def node_pool_create(
443447
:param update_strategy: The strategy to use when updating this node pool.
444448
NOTE: This field is specific to enterprise clusters.
445449
:type update_strategy: str
450+
:param disk_encryption: Local disk encryption setting for this LKE node pool.
451+
One of 'enabled' or 'disabled'. Defaults to 'disabled'.
452+
:type disk_encryption: str or InstanceDiskEncryptionType
446453
:param kwargs: Any other arguments to pass to the API. See the API docs
447454
for possible values.
448455
@@ -459,6 +466,7 @@ def node_pool_create(
459466
"taints": taints,
460467
"k8s_version": k8s_version,
461468
"update_strategy": update_strategy,
469+
"disk_encryption": disk_encryption,
462470
}
463471
params.update(kwargs)
464472

linode_api4/util.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
import string
6-
from typing import Any, Dict
6+
from typing import Any, Dict, List, Tuple, Union
77

88

99
def drop_null_keys(data: Dict[Any, Any], recursive=True) -> Dict[Any, Any]:
@@ -30,6 +30,13 @@ def recursive_helper(value: Any) -> Any:
3030
return recursive_helper(data)
3131

3232

33+
def normalize_as_list(value: Any) -> Union[List, Tuple]:
34+
"""
35+
Returns the value wrapped in a list if it isn't already a list or tuple.
36+
"""
37+
return value if isinstance(value, (list, tuple)) else [value]
38+
39+
3340
def generate_device_suffixes(n: int) -> list[str]:
3441
"""
3542
Generate n alphabetical suffixes starting with a, b, c, etc.

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ name = "linode_api4"
88
authors = [{ name = "Linode", email = "devs@linode.com" }]
99
description = "The official Python SDK for Linode API v4"
1010
readme = "README.rst"
11-
requires-python = ">=3.9"
11+
requires-python = ">=3.10"
1212
keywords = [
1313
"akamai",
1414
"Akamai Connected Cloud",
@@ -25,10 +25,11 @@ classifiers = [
2525
"License :: OSI Approved :: BSD License",
2626
"Programming Language :: Python",
2727
"Programming Language :: Python :: 3",
28-
"Programming Language :: Python :: 3.9",
2928
"Programming Language :: Python :: 3.10",
3029
"Programming Language :: Python :: 3.11",
3130
"Programming Language :: Python :: 3.12",
31+
"Programming Language :: Python :: 3.13",
32+
"Programming Language :: Python :: 3.14",
3233
]
3334
dependencies = ["requests", "polling", "deprecated"]
3435
dynamic = ["version"]
@@ -78,7 +79,7 @@ line_length = 80
7879

7980
[tool.black]
8081
line-length = 80
81-
target-version = ["py38", "py39", "py310", "py311", "py312"]
82+
target-version = ["py310", "py311", "py312", "py313", "py314"]
8283

8384
[tool.autoflake]
8485
expand-star-imports = true

test/integration/models/database/test_database.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ def test_update_sql_db(test_linode_client, test_create_sql_db):
230230

231231
assert res
232232
assert database.allow_list == new_allow_list
233+
# Label assertion is commented out because the API updates
234+
# the label intermittently, causing test failures. The issue
235+
# is tracked in TPT-4268.
233236
# assert database.label == label
234237
assert database.updates.day_of_week == 2
235238

@@ -354,7 +357,10 @@ def test_update_postgres_db(test_linode_client, test_create_postgres_db):
354357

355358
assert res
356359
assert database.allow_list == new_allow_list
357-
assert database.label == label
360+
# Label assertion is commented out because the API updates
361+
# the label intermittently, causing test failures. The issue
362+
# is tracked in TPT-4268.
363+
# assert database.label == label
358364
assert database.updates.day_of_week == 2
359365

360366

test/integration/models/database/test_database_engine_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def test_get_mysql_config(test_linode_client):
100100
assert isinstance(brp, dict)
101101
assert brp["type"] == "integer"
102102
assert brp["minimum"] == 600
103-
assert brp["maximum"] == 86400
103+
assert brp["maximum"] == 9007199254740991
104104
assert brp["requires_restart"] is False
105105

106106
# mysql sub-keys

test/integration/models/linode/test_linode.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,15 @@ def test_get_config(test_linode_client, create_linode):
790790
assert config.id == linode.configs[0].id
791791

792792

793+
def test_config_create_without_devices_raises_error(create_linode):
794+
linode = create_linode
795+
796+
with pytest.raises(ValueError) as err:
797+
linode.config_create(label="test-config-no-devices")
798+
799+
assert "Must include at least one disk or volume!" in str(err.value)
800+
801+
793802
def test_get_linode_types(test_linode_client):
794803
types = test_linode_client.linode.types()
795804

0 commit comments

Comments
 (0)