Skip to content

Commit 2a2788c

Browse files
authored
Merge branch 'linode:dev' into dev
2 parents 663c600 + dc3164c commit 2a2788c

File tree

12 files changed

+305
-75
lines changed

12 files changed

+305
-75
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
runs-on: ubuntu-latest
1515
steps:
1616
- name: checkout repo
17-
uses: actions/checkout@v5
17+
uses: actions/checkout@v6
1818

1919
- name: setup python 3
2020
uses: actions/setup-python@v6
@@ -33,7 +33,7 @@ jobs:
3333
matrix:
3434
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
3535
steps:
36-
- uses: actions/checkout@v5
36+
- uses: actions/checkout@v6
3737
- uses: actions/setup-python@v6
3838
with:
3939
python-version: ${{ matrix.python-version }}

.github/workflows/codeql.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
build-mode: none
2424
steps:
2525
- name: Checkout repository
26-
uses: actions/checkout@v5
26+
uses: actions/checkout@v6
2727

2828
- name: Initialize CodeQL
2929
uses: github/codeql-action/init@v3

.github/workflows/dependency-review.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- name: 'Checkout repository'
14-
uses: actions/checkout@v5
14+
uses: actions/checkout@v6
1515
- name: 'Dependency Review'
1616
uses: actions/dependency-review-action@v4
1717
with:

.github/workflows/e2e-test-pr.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848

4949
# Check out merge commit
5050
- name: Checkout PR
51-
uses: actions/checkout@v5
51+
uses: actions/checkout@v6
5252
with:
5353
ref: ${{ inputs.sha }}
5454
fetch-depth: 0
@@ -150,7 +150,7 @@ jobs:
150150

151151
steps:
152152
- name: Checkout code
153-
uses: actions/checkout@v5
153+
uses: actions/checkout@v6
154154
with:
155155
fetch-depth: 0
156156
submodules: 'recursive'

.github/workflows/e2e-test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ jobs:
5757
steps:
5858
- name: Clone Repository with SHA
5959
if: ${{ inputs.sha != '' }}
60-
uses: actions/checkout@v5
60+
uses: actions/checkout@v6
6161
with:
6262
fetch-depth: 0
6363
submodules: 'recursive'
6464
ref: ${{ inputs.sha }}
6565

6666
- name: Clone Repository without SHA
6767
if: ${{ inputs.sha == '' }}
68-
uses: actions/checkout@v5
68+
uses: actions/checkout@v6
6969
with:
7070
fetch-depth: 0
7171
submodules: 'recursive'
@@ -111,7 +111,7 @@ jobs:
111111

112112
steps:
113113
- name: Checkout code
114-
uses: actions/checkout@v5
114+
uses: actions/checkout@v6
115115
with:
116116
fetch-depth: 0
117117
submodules: 'recursive'
@@ -178,7 +178,7 @@ jobs:
178178

179179
steps:
180180
- name: Checkout code
181-
uses: actions/checkout@v5
181+
uses: actions/checkout@v6
182182
with:
183183
fetch-depth: 0
184184
submodules: 'recursive'

.github/workflows/labeler.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
steps:
1919
-
2020
name: Checkout
21-
uses: actions/checkout@v5
21+
uses: actions/checkout@v6
2222
-
2323
name: Run Labeler
2424
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916

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

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

2020
steps:
2121
- name: Checkout code
22-
uses: actions/checkout@v5
22+
uses: actions/checkout@v6
2323
with:
2424
ref: dev
2525

.github/workflows/publish-pypi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
environment: pypi-release
1313
steps:
1414
- name: Checkout
15-
uses: actions/checkout@v5
15+
uses: actions/checkout@v6
1616

1717
- name: Setup Python
1818
uses: actions/setup-python@v6

.github/workflows/release-cross-repo-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- name: Checkout linode_api4 repository
16-
uses: actions/checkout@v5
16+
uses: actions/checkout@v6
1717
with:
1818
fetch-depth: 0
1919
submodules: 'recursive'
@@ -30,7 +30,7 @@ jobs:
3030
python-version: '3.10'
3131

3232
- name: Checkout ansible repo
33-
uses: actions/checkout@v5
33+
uses: actions/checkout@v6
3434
with:
3535
repository: linode/ansible_linode
3636
path: .ansible/collections/ansible_collections/linode/cloud

linode_api4/objects/base.py

Lines changed: 98 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import time
22
from datetime import datetime, timedelta
3+
from functools import cached_property
34
from typing import Any, Dict, Optional
45

56
from linode_api4.objects.serializable import JSONObject
@@ -35,27 +36,43 @@ def __init__(
3536
nullable=False,
3637
unordered=False,
3738
json_object=None,
39+
alias_of: Optional[str] = None,
3840
):
3941
"""
4042
A Property is an attribute returned from the API, and defines metadata
41-
about that value. These are expected to be used as the values of a
43+
about that value. These are expected to be used as the values of a
4244
class-level dict named 'properties' in subclasses of Base.
4345
44-
mutable - This Property should be sent in a call to save()
45-
identifier - This Property identifies the object in the API
46-
volatile - Re-query for this Property if the local value is older than the
47-
volatile refresh timeout
48-
relationship - The API Object this Property represents
49-
derived_class - The sub-collection type this Property represents
50-
is_datetime - True if this Property should be parsed as a datetime.datetime
51-
id_relationship - This Property should create a relationship with this key as the ID
52-
(This should be used on fields ending with '_id' only)
53-
slug_relationship - This property is a slug related for a given type.
54-
nullable - This property can be explicitly null on PUT requests.
55-
unordered - The order of this property is not significant.
56-
NOTE: This field is currently only for annotations purposes
57-
and does not influence any update or decoding/encoding logic.
58-
json_object - The JSONObject class this property should be decoded into.
46+
:param mutable: This Property should be sent in a call to save()
47+
:type mutable: bool
48+
:param identifier: This Property identifies the object in the API
49+
:type identifier: bool
50+
:param volatile: Re-query for this Property if the local value is older than the
51+
volatile refresh timeout
52+
:type volatile: bool
53+
:param relationship: The API Object this Property represents
54+
:type relationship: type or None
55+
:param derived_class: The sub-collection type this Property represents
56+
:type derived_class: type or None
57+
:param is_datetime: True if this Property should be parsed as a datetime.datetime
58+
:type is_datetime: bool
59+
:param id_relationship: This Property should create a relationship with this key as the ID
60+
(This should be used on fields ending with '_id' only)
61+
:type id_relationship: type or None
62+
:param slug_relationship: This property is a slug related for a given type
63+
:type slug_relationship: type or None
64+
:param nullable: This property can be explicitly null on PUT requests
65+
:type nullable: bool
66+
:param unordered: The order of this property is not significant.
67+
NOTE: This field is currently only for annotations purposes
68+
and does not influence any update or decoding/encoding logic.
69+
:type unordered: bool
70+
:param json_object: The JSONObject class this property should be decoded into
71+
:type json_object: type or None
72+
:param alias_of: The original API attribute name when the property key is aliased.
73+
This is useful when the API attribute name is a Python reserved word,
74+
allowing you to use a different key while preserving the original name.
75+
:type alias_of: str or None
5976
"""
6077
self.mutable = mutable
6178
self.identifier = identifier
@@ -68,6 +85,7 @@ def __init__(
6885
self.nullable = nullable
6986
self.unordered = unordered
7087
self.json_class = json_object
88+
self.alias_of = alias_of
7189

7290

7391
class MappedObject:
@@ -252,6 +270,21 @@ def __setattr__(self, name, value):
252270

253271
self._set(name, value)
254272

273+
@cached_property
274+
def properties_with_alias(self) -> dict[str, tuple[str, Property]]:
275+
"""
276+
Gets a dictionary of aliased properties for this object.
277+
278+
:returns: A dict mapping original API attribute names to their alias names and
279+
corresponding Property instances.
280+
:rtype: dict[str, tuple[str, Property]]
281+
"""
282+
return {
283+
prop.alias_of: (alias, prop)
284+
for alias, prop in type(self).properties.items()
285+
if prop.alias_of
286+
}
287+
255288
def save(self, force=True) -> bool:
256289
"""
257290
Send this object's mutable values to the server in a PUT request.
@@ -345,7 +378,8 @@ def _serialize(self, is_put: bool = False):
345378
):
346379
value = None
347380

348-
result[k] = value
381+
api_key = k if not v.alias_of else v.alias_of
382+
result[api_key] = value
349383

350384
# Resolve the underlying IDs of results
351385
for k, v in result.items():
@@ -373,55 +407,55 @@ def _populate(self, json):
373407
self._set("_raw_json", json)
374408
self._set("_updated", False)
375409

376-
for key in json:
377-
if key in (
378-
k
379-
for k in type(self).properties.keys()
380-
if not type(self).properties[k].identifier
381-
):
382-
if (
383-
type(self).properties[key].relationship
384-
and not json[key] is None
385-
):
386-
if isinstance(json[key], list):
410+
valid_keys = set(
411+
k
412+
for k, v in type(self).properties.items()
413+
if (not v.identifier) and (not v.alias_of)
414+
) | set(self.properties_with_alias.keys())
415+
416+
for api_key in json:
417+
if api_key in valid_keys:
418+
prop = type(self).properties.get(api_key)
419+
prop_key = api_key
420+
421+
if prop is None:
422+
prop_key, prop = self.properties_with_alias[api_key]
423+
424+
if prop.relationship and json[api_key] is not None:
425+
if isinstance(json[api_key], list):
387426
objs = []
388-
for d in json[key]:
427+
for d in json[api_key]:
389428
if not "id" in d:
390429
continue
391-
new_class = type(self).properties[key].relationship
430+
new_class = prop.relationship
392431
obj = new_class.make_instance(
393432
d["id"], getattr(self, "_client")
394433
)
395434
if obj:
396435
obj._populate(d)
397436
objs.append(obj)
398-
self._set(key, objs)
437+
self._set(prop_key, objs)
399438
else:
400-
if isinstance(json[key], dict):
401-
related_id = json[key]["id"]
439+
if isinstance(json[api_key], dict):
440+
related_id = json[api_key]["id"]
402441
else:
403-
related_id = json[key]
404-
new_class = type(self).properties[key].relationship
442+
related_id = json[api_key]
443+
new_class = prop.relationship
405444
obj = new_class.make_instance(
406445
related_id, getattr(self, "_client")
407446
)
408-
if obj and isinstance(json[key], dict):
409-
obj._populate(json[key])
410-
self._set(key, obj)
411-
elif (
412-
type(self).properties[key].slug_relationship
413-
and not json[key] is None
414-
):
447+
if obj and isinstance(json[api_key], dict):
448+
obj._populate(json[api_key])
449+
self._set(prop_key, obj)
450+
elif prop.slug_relationship and json[api_key] is not None:
415451
# create an object of the expected type with the given slug
416452
self._set(
417-
key,
418-
type(self)
419-
.properties[key]
420-
.slug_relationship(self._client, json[key]),
453+
prop_key,
454+
prop.slug_relationship(self._client, json[api_key]),
421455
)
422-
elif type(self).properties[key].json_class:
423-
json_class = type(self).properties[key].json_class
424-
json_value = json[key]
456+
elif prop.json_class:
457+
json_class = prop.json_class
458+
json_value = json[api_key]
425459

426460
# build JSON object
427461
if isinstance(json_value, list):
@@ -430,25 +464,29 @@ def _populate(self, json):
430464
else:
431465
value = json_class.from_json(json_value)
432466

433-
self._set(key, value)
434-
elif type(json[key]) is dict:
435-
self._set(key, MappedObject(**json[key]))
436-
elif type(json[key]) is list:
467+
self._set(prop_key, value)
468+
elif type(json[api_key]) is dict:
469+
self._set(prop_key, MappedObject(**json[api_key]))
470+
elif type(json[api_key]) is list:
437471
# we're going to use MappedObject's behavior with lists to
438472
# expand these, then grab the resulting value to set
439-
mapping = MappedObject(_list=json[key])
440-
self._set(key, mapping._list) # pylint: disable=no-member
441-
elif type(self).properties[key].is_datetime:
473+
mapping = MappedObject(_list=json[api_key])
474+
self._set(
475+
prop_key, mapping._list
476+
) # pylint: disable=no-member
477+
elif prop.is_datetime:
442478
try:
443-
t = time.strptime(json[key], DATE_FORMAT)
444-
self._set(key, datetime.fromtimestamp(time.mktime(t)))
479+
t = time.strptime(json[api_key], DATE_FORMAT)
480+
self._set(
481+
prop_key, datetime.fromtimestamp(time.mktime(t))
482+
)
445483
except:
446484
# if this came back, there's probably an issue with the
447485
# python library; a field was marked as a datetime but
448486
# wasn't in the expected format.
449-
self._set(key, json[key])
487+
self._set(prop_key, json[api_key])
450488
else:
451-
self._set(key, json[key])
489+
self._set(prop_key, json[api_key])
452490

453491
self._set("_populated", True)
454492
self._set("_last_updated", datetime.now())

0 commit comments

Comments
 (0)