Skip to content

Commit 2162c5c

Browse files
committed
Merge branch 'dev' of https://github.com/linode/linode_api4-python into feat/aclp-list-entities
2 parents d671e08 + 02cd383 commit 2162c5c

File tree

15 files changed

+316
-87
lines changed

15 files changed

+316
-87
lines changed

.github/workflows/ci.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,35 @@ on:
1212
jobs:
1313
lint:
1414
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
pull-requests: read
1518
steps:
19+
# Enforce TPT-1234: prefix on PR titles, with the following exemptions:
20+
# - PRs labeled 'dependencies' (e.g. Dependabot PRs)
21+
# - PRs labeled 'hotfix' (urgent fixes that may not have a ticket)
22+
# - PRs labeled 'community-contribution' (external contributors without TPT tickets)
23+
# - PRs labeled 'ignore-for-release' (release PRs that don't need a ticket prefix)
24+
- name: Validate PR Title
25+
if: github.event_name == 'pull_request'
26+
uses: amannn/action-semantic-pull-request@v6
27+
with:
28+
types: |
29+
TPT-\d+
30+
requireScope: false
31+
# Override the default header pattern to allow hyphens and digits in the type
32+
# (e.g. "TPT-4298: Description"). The default pattern only matches word
33+
# characters (\w) which excludes hyphens.
34+
headerPattern: '^([\w-]+):\s?(.*)$'
35+
headerPatternCorrespondence: type, subject
36+
ignoreLabels: |
37+
dependencies
38+
hotfix
39+
community-contribution
40+
ignore-for-release
41+
env:
42+
GITHUB_TOKEN: ${{ github.token }}
43+
1644
- name: checkout repo
1745
uses: actions/checkout@v6
1846

@@ -31,7 +59,7 @@ jobs:
3159
runs-on: ubuntu-latest
3260
strategy:
3361
matrix:
34-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
62+
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
3563
steps:
3664
- uses: actions/checkout@v6
3765
- uses: actions/setup-python@v6
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Clean Release Notes
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
clean-release-notes:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
contents: write
12+
13+
steps:
14+
- name: Remove ticket prefixes from release notes
15+
uses: actions/github-script@v8
16+
with:
17+
script: |
18+
const release = context.payload.release;
19+
20+
let body = release.body;
21+
22+
if (!body) {
23+
console.log("Release body empty, nothing to clean.");
24+
return;
25+
}
26+
27+
// Remove ticket prefixes like "TPT-1234: " or "TPT-1234:"
28+
body = body.replace(/TPT-\d+:\s*/g, '');
29+
30+
await github.rest.repos.updateRelease({
31+
owner: context.repo.owner,
32+
repo: context.repo.repo,
33+
release_id: release.id,
34+
body: body
35+
});
36+
37+
console.log("Release notes cleaned.");

.github/workflows/e2e-test.yml

Lines changed: 5 additions & 5 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:
@@ -105,7 +105,7 @@ jobs:
105105

106106
- name: Upload Test Report as Artifact
107107
if: always()
108-
uses: actions/upload-artifact@v6
108+
uses: actions/upload-artifact@v7
109109
with:
110110
name: test-report-file
111111
if-no-files-found: ignore
@@ -241,7 +241,7 @@ jobs:
241241
steps:
242242
- name: Notify Slack
243243
id: main_message
244-
uses: slackapi/slack-github-action@v2.1.1
244+
uses: slackapi/slack-github-action@v3
245245
with:
246246
method: chat.postMessage
247247
token: ${{ secrets.SLACK_BOT_TOKEN }}
@@ -273,7 +273,7 @@ jobs:
273273
274274
- name: Test summary thread
275275
if: success()
276-
uses: slackapi/slack-github-action@v2.1.1
276+
uses: slackapi/slack-github-action@v3
277277
with:
278278
method: chat.postMessage
279279
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/nightly-smoke-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545

4646
- name: Notify Slack
4747
if: always() && github.repository == 'linode/linode_api4-python'
48-
uses: slackapi/slack-github-action@v2.1.1
48+
uses: slackapi/slack-github-action@v3
4949
with:
5050
method: chat.postMessage
5151
token: ${{ secrets.SLACK_BOT_TOKEN }}

.github/workflows/release-notify-slack.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
steps:
1212
- name: Notify Slack - Main Message
1313
id: main_message
14-
uses: slackapi/slack-github-action@v2.1.1
14+
uses: slackapi/slack-github-action@v3
1515
with:
1616
method: chat.postMessage
1717
token: ${{ secrets.SLACK_BOT_TOKEN }}

linode_api4/groups/monitor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def alert_channels(self, *filters) -> PaginatedList:
204204
205205
.. note:: This endpoint is in beta and requires using the v4beta base URL.
206206
207-
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-alert-channels
207+
API Documentation: https://techdocs.akamai.com/linode-api/reference/get-notification-channels
208208
209209
:param filters: Optional filter expressions to apply to the collection.
210210
See :doc:`Filtering Collections</linode_api4/objects/filtering>` for details.

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(

0 commit comments

Comments
 (0)