Skip to content

Commit 6a457de

Browse files
various changes
1 parent a70daed commit 6a457de

File tree

5 files changed

+110
-89
lines changed

5 files changed

+110
-89
lines changed

linode_api4/objects/account.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ def entity(self):
601601
)
602602
return self.cls(self._client, self.id)
603603

604-
def _serialize(self):
604+
def _serialize(self, *args, **kwargs):
605605
"""
606606
Returns this grant in as JSON the api will accept. This is only relevant
607607
in the context of UserGrants.save
@@ -668,7 +668,7 @@ def _grants_dict(self):
668668

669669
return grants
670670

671-
def _serialize(self):
671+
def _serialize(self, *args, **kwargs):
672672
"""
673673
Returns the user grants in as JSON the api will accept.
674674
This is only relevant in the context of UserGrants.save

linode_api4/objects/base.py

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,6 @@ def __init__(
5757
NOTE: This field is currently only for annotations purposes
5858
and does not influence any update or decoding/encoding logic.
5959
json_object - The JSONObject class this property should be decoded into.
60-
json_object_options - The JSONObject class this property should use when
61-
serializing for PUT requests.
6260
"""
6361
self.mutable = mutable
6462
self.identifier = identifier
@@ -71,7 +69,6 @@ def __init__(
7169
self.nullable = nullable
7270
self.unordered = unordered
7371
self.json_class = json_object
74-
self.json_class_options = json_object_options
7572

7673

7774
class MappedObject:
@@ -118,6 +115,9 @@ def _flatten_base_subclass(obj: "Base") -> Optional[Dict[str, Any]]:
118115

119116
@property
120117
def dict(self):
118+
return self._serialize()
119+
120+
def _serialize(self, is_put: bool = False) -> Dict[str, Any]:
121121
result = vars(self).copy()
122122
cls = type(self)
123123

@@ -127,7 +127,7 @@ def dict(self):
127127
elif isinstance(v, list):
128128
result[k] = [
129129
(
130-
item.dict
130+
item._serialize(is_put=is_put)
131131
if isinstance(item, (cls, JSONObject))
132132
else (
133133
self._flatten_base_subclass(item)
@@ -140,7 +140,7 @@ def dict(self):
140140
elif isinstance(v, Base):
141141
result[k] = self._flatten_base_subclass(v)
142142
elif isinstance(v, JSONObject):
143-
result[k] = v.dict
143+
result[k] = v._serialize(is_put=is_put)
144144

145145
return result
146146

@@ -282,20 +282,9 @@ def save(self, force=True) -> bool:
282282
data[key] = None
283283

284284
# Ensure we serialize any values that may not be already serialized
285-
data = _flatten_request_body_recursive(data)
285+
data = _flatten_request_body_recursive(data, is_put=True)
286286
else:
287-
data = self._serialize()
288-
289-
# Hack to minimize PUT contents to the configured json_class_options schema
290-
for key, value in data.items():
291-
if value is None or key not in type(self).properties:
292-
continue
293-
294-
json_class_options = type(self).properties[key].json_class_options
295-
if json_class_options is None:
296-
continue
297-
298-
data[key] = json_class_options.from_json(value).dict
287+
data = self._serialize(is_put=True)
299288

300289
resp = self._client.put(type(self).api_endpoint, model=self, data=data)
301290

@@ -331,7 +320,7 @@ def invalidate(self):
331320

332321
self._set("_populated", False)
333322

334-
def _serialize(self):
323+
def _serialize(self, is_put: bool = False):
335324
"""
336325
A helper method to build a dict of all mutable Properties of
337326
this object
@@ -360,7 +349,7 @@ def _serialize(self):
360349

361350
# Resolve the underlying IDs of results
362351
for k, v in result.items():
363-
result[k] = _flatten_request_body_recursive(v)
352+
result[k] = _flatten_request_body_recursive(v, is_put=is_put)
364353

365354
return result
366355

@@ -518,7 +507,7 @@ def make_instance(cls, id, client, parent_id=None, json=None):
518507
return Base.make(id, client, cls, parent_id=parent_id, json=json)
519508

520509

521-
def _flatten_request_body_recursive(data: Any) -> Any:
510+
def _flatten_request_body_recursive(data: Any, is_put: bool = False) -> Any:
522511
"""
523512
This is a helper recursively flatten the given data for use in an API request body.
524513
@@ -530,15 +519,18 @@ def _flatten_request_body_recursive(data: Any) -> Any:
530519
"""
531520

532521
if isinstance(data, dict):
533-
return {k: _flatten_request_body_recursive(v) for k, v in data.items()}
522+
return {
523+
k: _flatten_request_body_recursive(v, is_put=is_put)
524+
for k, v in data.items()
525+
}
534526

535527
if isinstance(data, list):
536-
return [_flatten_request_body_recursive(v) for v in data]
528+
return [_flatten_request_body_recursive(v, is_put=is_put) for v in data]
537529

538530
if isinstance(data, Base):
539531
return data.id
540532

541533
if isinstance(data, MappedObject) or issubclass(type(data), JSONObject):
542-
return data.dict
534+
return data._serialize(is_put=is_put)
543535

544536
return data

linode_api4/objects/linode.py

Lines changed: 38 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -296,41 +296,47 @@ class ConfigInterfaceIPv4(JSONObject):
296296

297297

298298
@dataclass
299-
class ConfigInterfaceIPv6SLAAC(JSONObject):
299+
class ConfigInterfaceIPv6SLAACOptions(JSONObject):
300300
range: str = ""
301-
address: str = ""
302301

303302

304303
@dataclass
305-
class ConfigInterfaceIPv6Range(JSONObject):
304+
class ConfigInterfaceIPv6RangeOptions(JSONObject):
306305
range: str = ""
307306

308307

309308
@dataclass
310-
class ConfigInterfaceIPv6(JSONObject):
311-
slaac: List[ConfigInterfaceIPv6SLAAC] = field(default_factory=lambda: [])
312-
ranges: List[ConfigInterfaceIPv6Range] = field(default_factory=lambda: [])
309+
class ConfigInterfaceIPv6Options(JSONObject):
310+
slaac: List[ConfigInterfaceIPv6SLAACOptions] = field(
311+
default_factory=lambda: []
312+
)
313+
ranges: List[ConfigInterfaceIPv6RangeOptions] = field(
314+
default_factory=lambda: []
315+
)
313316
is_public: bool = False
314317

315318

316319
@dataclass
317-
class ConfigInterfaceIPv6SLAACOptions(JSONObject):
320+
class ConfigInterfaceIPv6SLAAC(JSONObject):
321+
put_class = ConfigInterfaceIPv6SLAACOptions
322+
318323
range: str = ""
324+
address: str = ""
319325

320326

321327
@dataclass
322-
class ConfigInterfaceIPv6RangeOptions(JSONObject):
328+
class ConfigInterfaceIPv6Range(JSONObject):
329+
put_class = ConfigInterfaceIPv6RangeOptions
330+
323331
range: str = ""
324332

325333

326334
@dataclass
327-
class ConfigInterfaceIPv6Options(JSONObject):
328-
slaac: List[ConfigInterfaceIPv6SLAACOptions] = field(
329-
default_factory=lambda: []
330-
)
331-
ranges: List[ConfigInterfaceIPv6RangeOptions] = field(
332-
default_factory=lambda: []
333-
)
335+
class ConfigInterfaceIPv6(JSONObject):
336+
put_class = ConfigInterfaceIPv6Options
337+
338+
slaac: List[ConfigInterfaceIPv6SLAAC] = field(default_factory=lambda: [])
339+
ranges: List[ConfigInterfaceIPv6Range] = field(default_factory=lambda: [])
334340
is_public: bool = False
335341

336342

@@ -359,11 +365,7 @@ class NetworkInterface(DerivedBase):
359365
"vpc_id": Property(id_relationship=VPC),
360366
"subnet_id": Property(),
361367
"ipv4": Property(mutable=True, json_object=ConfigInterfaceIPv4),
362-
"ipv6": Property(
363-
mutable=True,
364-
json_object=ConfigInterfaceIPv6,
365-
json_object_options=ConfigInterfaceIPv6Options,
366-
),
368+
"ipv6": Property(mutable=True, json_object=ConfigInterfaceIPv6),
367369
"ip_ranges": Property(mutable=True),
368370
}
369371

@@ -389,17 +391,6 @@ def __init__(self, client, id, parent_id, instance_id=None, json=None):
389391
def __repr__(self):
390392
return f"Interface: {self.purpose} {self.id}"
391393

392-
def _serialize(self):
393-
result = DerivedBase._serialize(self)
394-
395-
ipv6 = result.get("ipv6", None)
396-
397-
if isinstance(ipv6, ConfigInterfaceIPv6):
398-
print("SDF")
399-
result["ipv6"] = ConfigInterfaceIPv6Options.from_json(ipv6.dict)
400-
401-
return result
402-
403394
@property
404395
def subnet(self) -> VPCSubnet:
405396
"""
@@ -458,7 +449,7 @@ class ConfigInterface(JSONObject):
458449
def __repr__(self):
459450
return f"Interface: {self.purpose}"
460451

461-
def _serialize(self):
452+
def _serialize(self, is_put: bool = False):
462453
purpose_formats = {
463454
"public": {"purpose": "public", "primary": self.primary},
464455
"vlan": {
@@ -470,16 +461,8 @@ def _serialize(self):
470461
"purpose": "vpc",
471462
"primary": self.primary,
472463
"subnet_id": self.subnet_id,
473-
"ipv4": (
474-
self.ipv4.dict
475-
if isinstance(self.ipv4, ConfigInterfaceIPv4)
476-
else self.ipv4
477-
),
478-
"ipv6": (
479-
self.ipv6.dict
480-
if isinstance(self.ipv6, ConfigInterfaceIPv6)
481-
else self.ipv6
482-
),
464+
"ipv4": self.ipv4,
465+
"ipv6": self.ipv6,
483466
"ip_ranges": self.ip_ranges,
484467
},
485468
}
@@ -489,11 +472,14 @@ def _serialize(self):
489472
f"Unknown interface purpose: {self.purpose}",
490473
)
491474

492-
return {
493-
k: v
494-
for k, v in purpose_formats[self.purpose].items()
495-
if v is not None
496-
}
475+
return _flatten_request_body_recursive(
476+
{
477+
k: v
478+
for k, v in purpose_formats[self.purpose].items()
479+
if v is not None
480+
},
481+
is_put=is_put,
482+
)
497483

498484

499485
class Config(DerivedBase):
@@ -573,16 +559,16 @@ def _populate(self, json):
573559

574560
self._set("devices", MappedObject(**devices))
575561

576-
def _serialize(self):
562+
def _serialize(self, *args, **kwargs):
577563
"""
578564
Overrides _serialize to transform interfaces into json
579565
"""
580-
partial = DerivedBase._serialize(self)
566+
partial = DerivedBase._serialize(self, *args, **kwargs)
581567
interfaces = []
582568

583569
for c in self.interfaces:
584570
if isinstance(c, ConfigInterface):
585-
interfaces.append(c._serialize())
571+
interfaces.append(c._serialize(*args, **kwargs))
586572
else:
587573
interfaces.append(c)
588574

@@ -1990,8 +1976,8 @@ def _populate(self, json):
19901976
ndist = [Image(self._client, d) for d in self.images]
19911977
self._set("images", ndist)
19921978

1993-
def _serialize(self):
1994-
dct = Base._serialize(self)
1979+
def _serialize(self, *args, **kwargs):
1980+
dct = Base._serialize(self, *args, **kwargs)
19951981
dct["images"] = [d.id for d in self.images]
19961982
return dct
19971983

linode_api4/objects/serializable.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import inspect
2-
from dataclasses import dataclass
2+
from dataclasses import dataclass, fields
33
from enum import Enum
44
from types import SimpleNamespace
55
from typing import (
@@ -9,6 +9,7 @@
99
List,
1010
Optional,
1111
Set,
12+
Type,
1213
Union,
1314
get_args,
1415
get_origin,
@@ -71,6 +72,13 @@ class JSONObject(metaclass=JSONFilterableMetaclass):
7172
are None.
7273
"""
7374

75+
put_class: ClassVar[Optional[Type["JSONObject"]]] = None
76+
"""
77+
An alternative JSONObject class to use as the schema for PUT requests.
78+
This prevents read-only fields from being included in PUT request bodies,
79+
which in theory will result in validation errors from the API.
80+
"""
81+
7482
def __init__(self):
7583
raise NotImplementedError(
7684
"JSONObject is not intended to be constructed directly"
@@ -154,19 +162,25 @@ def from_json(cls, json: Dict[str, Any]) -> Optional["JSONObject"]:
154162

155163
return obj
156164

157-
def _serialize(self) -> Dict[str, Any]:
165+
def _serialize(self, is_put: bool = False) -> Dict[str, Any]:
158166
"""
159167
Serializes this object into a JSON dict.
160168
"""
161169
cls = type(self)
170+
171+
if is_put and cls.put_class is not None:
172+
cls = cls.put_class
173+
174+
cls_field_keys = {field.name for field in fields(cls)}
175+
162176
type_hints = get_type_hints(cls)
163177

164178
def attempt_serialize(value: Any) -> Any:
165179
"""
166180
Attempts to serialize the given value, else returns the value unchanged.
167181
"""
168182
if issubclass(type(value), JSONObject):
169-
return value._serialize()
183+
return value._serialize(is_put=is_put)
170184

171185
return value
172186

@@ -175,6 +189,10 @@ def should_include(key: str, value: Any) -> bool:
175189
Returns whether the given key/value pair should be included in the resulting dict.
176190
"""
177191

192+
# During PUT operations, keys not present in the put_class should be excluded
193+
if key not in cls_field_keys:
194+
return False
195+
178196
if cls.include_none_values or key in cls.always_include:
179197
return True
180198

0 commit comments

Comments
 (0)