11import time
22from datetime import datetime , timedelta
3+ from functools import cached_property
34from typing import Any , Dict , Optional
45
56from 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
7391class 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