11import time
22from datetime import datetime , timedelta
3+ from functools import cached_property
4+ from itertools import chain
35from typing import Any , Dict , Optional
46
57from linode_api4 .objects .serializable import JSONObject
@@ -35,27 +37,43 @@ def __init__(
3537 nullable = False ,
3638 unordered = False ,
3739 json_object = None ,
40+ alias_of : Optional [str ] = None ,
3841 ):
3942 """
4043 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
44+ about that value. These are expected to be used as the values of a
4245 class-level dict named 'properties' in subclasses of Base.
4346
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.
47+ :param mutable: This Property should be sent in a call to save()
48+ :type mutable: bool
49+ :param identifier: This Property identifies the object in the API
50+ :type identifier: bool
51+ :param volatile: Re-query for this Property if the local value is older than the
52+ volatile refresh timeout
53+ :type volatile: bool
54+ :param relationship: The API Object this Property represents
55+ :type relationship: type or None
56+ :param derived_class: The sub-collection type this Property represents
57+ :type derived_class: type or None
58+ :param is_datetime: True if this Property should be parsed as a datetime.datetime
59+ :type is_datetime: bool
60+ :param id_relationship: This Property should create a relationship with this key as the ID
61+ (This should be used on fields ending with '_id' only)
62+ :type id_relationship: type or None
63+ :param slug_relationship: This property is a slug related for a given type
64+ :type slug_relationship: type or None
65+ :param nullable: This property can be explicitly null on PUT requests
66+ :type nullable: bool
67+ :param unordered: The order of this property is not significant.
68+ NOTE: This field is currently only for annotations purposes
69+ and does not influence any update or decoding/encoding logic.
70+ :type unordered: bool
71+ :param json_object: The JSONObject class this property should be decoded into
72+ :type json_object: type or None
73+ :param alias_of: The original API attribute name when the property key is aliased.
74+ This is useful when the API attribute name is a Python reserved word,
75+ allowing you to use a different key while preserving the original name.
76+ :type alias_of: str or None
5977 """
6078 self .mutable = mutable
6179 self .identifier = identifier
@@ -68,6 +86,7 @@ def __init__(
6886 self .nullable = nullable
6987 self .unordered = unordered
7088 self .json_class = json_object
89+ self .alias_of = alias_of
7190
7291
7392class MappedObject :
@@ -252,6 +271,21 @@ def __setattr__(self, name, value):
252271
253272 self ._set (name , value )
254273
274+ @cached_property
275+ def properties_with_alias (self ) -> dict [str , tuple [str , Property ]]:
276+ """
277+ Gets a dictionary of aliased properties for this object.
278+
279+ :returns: A dict mapping original API attribute names to their alias names and
280+ corresponding Property instances.
281+ :rtype: dict[str, tuple[str, Property]]
282+ """
283+ return {
284+ prop .alias_of : (alias , prop )
285+ for alias , prop in type (self ).properties .items ()
286+ if prop .alias_of
287+ }
288+
255289 def save (self , force = True ) -> bool :
256290 """
257291 Send this object's mutable values to the server in a PUT request.
@@ -345,7 +379,8 @@ def _serialize(self, is_put: bool = False):
345379 ):
346380 value = None
347381
348- result [k ] = value
382+ api_key = k if not v .alias_of else v .alias_of
383+ result [api_key ] = value
349384
350385 # Resolve the underlying IDs of results
351386 for k , v in result .items ():
@@ -373,55 +408,56 @@ def _populate(self, json):
373408 self ._set ("_raw_json" , json )
374409 self ._set ("_updated" , False )
375410
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
411+ for api_key in json :
412+ if api_key in chain (
413+ (
414+ k
415+ for k , v in type (self ).properties .items ()
416+ if (not v .identifier ) and (not v .alias_of )
417+ ), # Exclude identifiers and aliased properties to avoid conflicts with API attributes
418+ self .properties_with_alias .keys (),
381419 ):
382- if (
383- type (self ).properties [key ].relationship
384- and not json [key ] is None
385- ):
386- if isinstance (json [key ], list ):
420+ prop = type (self ).properties .get (api_key )
421+ prop_key = api_key
422+
423+ if prop is None :
424+ prop_key , prop = self .properties_with_alias [api_key ]
425+
426+ if prop .relationship and not json [api_key ] is None :
427+ if isinstance (json [api_key ], list ):
387428 objs = []
388- for d in json [key ]:
429+ for d in json [api_key ]:
389430 if not "id" in d :
390431 continue
391- new_class = type ( self ). properties [ key ] .relationship
432+ new_class = prop .relationship
392433 obj = new_class .make_instance (
393434 d ["id" ], getattr (self , "_client" )
394435 )
395436 if obj :
396437 obj ._populate (d )
397438 objs .append (obj )
398- self ._set (key , objs )
439+ self ._set (prop_key , objs )
399440 else :
400- if isinstance (json [key ], dict ):
401- related_id = json [key ]["id" ]
441+ if isinstance (json [api_key ], dict ):
442+ related_id = json [api_key ]["id" ]
402443 else :
403- related_id = json [key ]
404- new_class = type ( self ). properties [ key ] .relationship
444+ related_id = json [api_key ]
445+ new_class = prop .relationship
405446 obj = new_class .make_instance (
406447 related_id , getattr (self , "_client" )
407448 )
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- ):
449+ if obj and isinstance (json [api_key ], dict ):
450+ obj ._populate (json [api_key ])
451+ self ._set (prop_key , obj )
452+ elif prop .slug_relationship and not json [api_key ] is None :
415453 # create an object of the expected type with the given slug
416454 self ._set (
417- key ,
418- type (self )
419- .properties [key ]
420- .slug_relationship (self ._client , json [key ]),
455+ prop_key ,
456+ prop .slug_relationship (self ._client , json [api_key ]),
421457 )
422- elif type ( self ). properties [ key ] .json_class :
423- json_class = type ( self ). properties [ key ] .json_class
424- json_value = json [key ]
458+ elif prop .json_class :
459+ json_class = prop .json_class
460+ json_value = json [api_key ]
425461
426462 # build JSON object
427463 if isinstance (json_value , list ):
@@ -430,25 +466,29 @@ def _populate(self, json):
430466 else :
431467 value = json_class .from_json (json_value )
432468
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 :
469+ self ._set (prop_key , value )
470+ elif type (json [api_key ]) is dict :
471+ self ._set (prop_key , MappedObject (** json [api_key ]))
472+ elif type (json [api_key ]) is list :
437473 # we're going to use MappedObject's behavior with lists to
438474 # 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 :
475+ mapping = MappedObject (_list = json [api_key ])
476+ self ._set (
477+ prop_key , mapping ._list
478+ ) # pylint: disable=no-member
479+ elif prop .is_datetime :
442480 try :
443- t = time .strptime (json [key ], DATE_FORMAT )
444- self ._set (key , datetime .fromtimestamp (time .mktime (t )))
481+ t = time .strptime (json [api_key ], DATE_FORMAT )
482+ self ._set (
483+ prop_key , datetime .fromtimestamp (time .mktime (t ))
484+ )
445485 except :
446486 # if this came back, there's probably an issue with the
447487 # python library; a field was marked as a datetime but
448488 # wasn't in the expected format.
449- self ._set (key , json [key ])
489+ self ._set (prop_key , json [api_key ])
450490 else :
451- self ._set (key , json [key ])
491+ self ._set (prop_key , json [api_key ])
452492
453493 self ._set ("_populated" , True )
454494 self ._set ("_last_updated" , datetime .now ())
0 commit comments