From 34eed03c5830d1525a436e3d2b1811e06c9fb12d Mon Sep 17 00:00:00 2001 From: Pawel <100145168+PawelSnoch@users.noreply.github.com> Date: Fri, 10 Oct 2025 08:43:21 +0200 Subject: [PATCH 1/3] Add test get not supported service (#609) --- test/integration/models/monitor/test_monitor.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/integration/models/monitor/test_monitor.py b/test/integration/models/monitor/test_monitor.py index 7c9249f42..b458fd399 100644 --- a/test/integration/models/monitor/test_monitor.py +++ b/test/integration/models/monitor/test_monitor.py @@ -55,6 +55,13 @@ def test_get_supported_services(test_linode_client): assert isinstance(metric_definitions[0], MonitorMetricsDefinition) +def test_get_not_supported_service(test_linode_client): + client = test_linode_client + with pytest.raises(RuntimeError) as err: + client.load(MonitorService, "saas") + assert "[404] Not found" in str(err.value) + + # Test Helpers def get_db_engine_id(client: LinodeClient, engine: str): engines = client.database.engines() From be4afe83d14e5f159b947efd51f5e9a694f494f8 Mon Sep 17 00:00:00 2001 From: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com> Date: Mon, 27 Oct 2025 13:55:55 -0400 Subject: [PATCH 2/3] Handle explicit null value in `_flatten_request_body_recursive` (#612) * Accept explicit null value in `_flatten_request_body_recursive` * Add test * make format --- linode_api4/objects/base.py | 3 + test/unit/objects/base_test.py | 286 +++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+) create mode 100644 test/unit/objects/base_test.py diff --git a/linode_api4/objects/base.py b/linode_api4/objects/base.py index 51a16eae0..9f2a55589 100644 --- a/linode_api4/objects/base.py +++ b/linode_api4/objects/base.py @@ -530,6 +530,9 @@ def _flatten_request_body_recursive(data: Any, is_put: bool = False) -> Any: if isinstance(data, Base): return data.id + if isinstance(data, ExplicitNullValue) or data == ExplicitNullValue: + return None + if isinstance(data, MappedObject) or issubclass(type(data), JSONObject): return data._serialize(is_put=is_put) diff --git a/test/unit/objects/base_test.py b/test/unit/objects/base_test.py new file mode 100644 index 000000000..d60a3bd38 --- /dev/null +++ b/test/unit/objects/base_test.py @@ -0,0 +1,286 @@ +from dataclasses import dataclass +from test.unit.base import ClientBaseCase + +from linode_api4.objects import Base, JSONObject, MappedObject, Property +from linode_api4.objects.base import ( + ExplicitNullValue, + _flatten_request_body_recursive, +) + + +class FlattenRequestBodyRecursiveCase(ClientBaseCase): + """Test cases for _flatten_request_body_recursive function""" + + def test_flatten_primitive_types(self): + """Test that primitive types are returned as-is""" + self.assertEqual(_flatten_request_body_recursive(123), 123) + self.assertEqual(_flatten_request_body_recursive("test"), "test") + self.assertEqual(_flatten_request_body_recursive(3.14), 3.14) + self.assertEqual(_flatten_request_body_recursive(True), True) + self.assertEqual(_flatten_request_body_recursive(False), False) + self.assertEqual(_flatten_request_body_recursive(None), None) + + def test_flatten_dict(self): + """Test that dicts are recursively flattened""" + test_dict = {"key1": "value1", "key2": 123, "key3": True} + result = _flatten_request_body_recursive(test_dict) + self.assertEqual(result, test_dict) + + def test_flatten_nested_dict(self): + """Test that nested dicts are recursively flattened""" + test_dict = { + "level1": { + "level2": {"level3": "value", "number": 42}, + "string": "test", + }, + "array": [1, 2, 3], + } + result = _flatten_request_body_recursive(test_dict) + self.assertEqual(result, test_dict) + + def test_flatten_list(self): + """Test that lists are recursively flattened""" + test_list = [1, "two", 3.0, True] + result = _flatten_request_body_recursive(test_list) + self.assertEqual(result, test_list) + + def test_flatten_nested_list(self): + """Test that nested lists are recursively flattened""" + test_list = [[1, 2], [3, [4, 5]], "string"] + result = _flatten_request_body_recursive(test_list) + self.assertEqual(result, test_list) + + def test_flatten_base_object(self): + """Test that Base objects are flattened to their ID""" + + class TestBase(Base): + api_endpoint = "/test/{id}" + properties = { + "id": Property(identifier=True), + "label": Property(mutable=True), + } + + obj = TestBase(self.client, 123) + result = _flatten_request_body_recursive(obj) + self.assertEqual(result, 123) + + def test_flatten_base_object_in_dict(self): + """Test that Base objects in dicts are flattened to their ID""" + + class TestBase(Base): + api_endpoint = "/test/{id}" + properties = { + "id": Property(identifier=True), + "label": Property(mutable=True), + } + + obj = TestBase(self.client, 456) + test_dict = {"resource": obj, "name": "test"} + result = _flatten_request_body_recursive(test_dict) + self.assertEqual(result, {"resource": 456, "name": "test"}) + + def test_flatten_base_object_in_list(self): + """Test that Base objects in lists are flattened to their ID""" + + class TestBase(Base): + api_endpoint = "/test/{id}" + properties = { + "id": Property(identifier=True), + "label": Property(mutable=True), + } + + obj1 = TestBase(self.client, 111) + obj2 = TestBase(self.client, 222) + test_list = [obj1, "middle", obj2] + result = _flatten_request_body_recursive(test_list) + self.assertEqual(result, [111, "middle", 222]) + + def test_flatten_explicit_null_instance(self): + """Test that ExplicitNullValue instances are converted to None""" + result = _flatten_request_body_recursive(ExplicitNullValue()) + self.assertIsNone(result) + + def test_flatten_explicit_null_class(self): + """Test that ExplicitNullValue class is converted to None""" + result = _flatten_request_body_recursive(ExplicitNullValue) + self.assertIsNone(result) + + def test_flatten_explicit_null_in_dict(self): + """Test that ExplicitNullValue in dicts is converted to None""" + test_dict = { + "field1": "value", + "field2": ExplicitNullValue(), + "field3": ExplicitNullValue, + } + result = _flatten_request_body_recursive(test_dict) + self.assertEqual( + result, {"field1": "value", "field2": None, "field3": None} + ) + + def test_flatten_explicit_null_in_list(self): + """Test that ExplicitNullValue in lists is converted to None""" + test_list = ["value", ExplicitNullValue(), ExplicitNullValue, 123] + result = _flatten_request_body_recursive(test_list) + self.assertEqual(result, ["value", None, None, 123]) + + def test_flatten_mapped_object(self): + """Test that MappedObject is serialized""" + mapped_obj = MappedObject(key1="value1", key2=123) + result = _flatten_request_body_recursive(mapped_obj) + self.assertEqual(result, {"key1": "value1", "key2": 123}) + + def test_flatten_mapped_object_nested(self): + """Test that nested MappedObject is serialized""" + mapped_obj = MappedObject( + outer="value", inner={"nested_key": "nested_value"} + ) + result = _flatten_request_body_recursive(mapped_obj) + # The inner dict becomes a MappedObject when created + self.assertIn("outer", result) + self.assertEqual(result["outer"], "value") + self.assertIn("inner", result) + + def test_flatten_mapped_object_in_dict(self): + """Test that MappedObject in dicts is serialized""" + mapped_obj = MappedObject(key="value") + test_dict = {"field": mapped_obj, "other": "data"} + result = _flatten_request_body_recursive(test_dict) + self.assertEqual(result, {"field": {"key": "value"}, "other": "data"}) + + def test_flatten_mapped_object_in_list(self): + """Test that MappedObject in lists is serialized""" + mapped_obj = MappedObject(key="value") + test_list = [mapped_obj, "string", 123] + result = _flatten_request_body_recursive(test_list) + self.assertEqual(result, [{"key": "value"}, "string", 123]) + + def test_flatten_json_object(self): + """Test that JSONObject subclasses are serialized""" + + @dataclass + class TestJSONObject(JSONObject): + field1: str = "" + field2: int = 0 + + json_obj = TestJSONObject.from_json({"field1": "test", "field2": 42}) + result = _flatten_request_body_recursive(json_obj) + self.assertEqual(result, {"field1": "test", "field2": 42}) + + def test_flatten_json_object_in_dict(self): + """Test that JSONObject in dicts is serialized""" + + @dataclass + class TestJSONObject(JSONObject): + name: str = "" + + json_obj = TestJSONObject.from_json({"name": "test"}) + test_dict = {"obj": json_obj, "value": 123} + result = _flatten_request_body_recursive(test_dict) + self.assertEqual(result, {"obj": {"name": "test"}, "value": 123}) + + def test_flatten_json_object_in_list(self): + """Test that JSONObject in lists is serialized""" + + @dataclass + class TestJSONObject(JSONObject): + id: int = 0 + + json_obj = TestJSONObject.from_json({"id": 999}) + test_list = [json_obj, "text"] + result = _flatten_request_body_recursive(test_list) + self.assertEqual(result, [{"id": 999}, "text"]) + + def test_flatten_complex_nested_structure(self): + """Test a complex nested structure with multiple types""" + + class TestBase(Base): + api_endpoint = "/test/{id}" + properties = { + "id": Property(identifier=True), + } + + @dataclass + class TestJSONObject(JSONObject): + value: str = "" + + base_obj = TestBase(self.client, 555) + mapped_obj = MappedObject(key="mapped") + json_obj = TestJSONObject.from_json({"value": "json"}) + + complex_structure = { + "base": base_obj, + "mapped": mapped_obj, + "json": json_obj, + "null": ExplicitNullValue(), + "list": [base_obj, mapped_obj, json_obj, ExplicitNullValue], + "nested": { + "deep": { + "base": base_obj, + "primitives": [1, "two", 3.0], + } + }, + } + + result = _flatten_request_body_recursive(complex_structure) + + self.assertEqual(result["base"], 555) + self.assertEqual(result["mapped"], {"key": "mapped"}) + self.assertEqual(result["json"], {"value": "json"}) + self.assertIsNone(result["null"]) + self.assertEqual( + result["list"], [555, {"key": "mapped"}, {"value": "json"}, None] + ) + self.assertEqual(result["nested"]["deep"]["base"], 555) + self.assertEqual( + result["nested"]["deep"]["primitives"], [1, "two", 3.0] + ) + + def test_flatten_with_is_put_false(self): + """Test that is_put parameter is passed through""" + + @dataclass + class TestJSONObject(JSONObject): + field: str = "" + + def _serialize(self, is_put=False): + return {"field": self.field, "is_put": is_put} + + json_obj = TestJSONObject.from_json({"field": "test"}) + result = _flatten_request_body_recursive(json_obj, is_put=False) + self.assertEqual(result, {"field": "test", "is_put": False}) + + def test_flatten_with_is_put_true(self): + """Test that is_put=True parameter is passed through""" + + @dataclass + class TestJSONObject(JSONObject): + field: str = "" + + def _serialize(self, is_put=False): + return {"field": self.field, "is_put": is_put} + + json_obj = TestJSONObject.from_json({"field": "test"}) + result = _flatten_request_body_recursive(json_obj, is_put=True) + self.assertEqual(result, {"field": "test", "is_put": True}) + + def test_flatten_empty_dict(self): + """Test that empty dicts are handled correctly""" + result = _flatten_request_body_recursive({}) + self.assertEqual(result, {}) + + def test_flatten_empty_list(self): + """Test that empty lists are handled correctly""" + result = _flatten_request_body_recursive([]) + self.assertEqual(result, []) + + def test_flatten_dict_with_none_values(self): + """Test that None values in dicts are preserved""" + test_dict = {"key1": "value", "key2": None, "key3": 0} + result = _flatten_request_body_recursive(test_dict) + self.assertEqual(result, test_dict) + + def test_flatten_list_with_none_values(self): + """Test that None values in lists are preserved""" + test_list = ["value", None, 0, ""] + result = _flatten_request_body_recursive(test_list) + self.assertEqual(result, test_list) From 06b09b8234420b8bc3e1480e06cdf8c8c0a08998 Mon Sep 17 00:00:00 2001 From: rammanoj Date: Mon, 27 Oct 2025 15:29:24 -0400 Subject: [PATCH 3/3] Add firewall_id to LNP (#615) * add firewall_id * Add to integration tests --------- Co-authored-by: Erik Zilber Co-authored-by: rpotla Co-authored-by: Lena Garber --- linode_api4/objects/lke.py | 1 + test/fixtures/lke_clusters_18881_pools_456.json | 1 + test/fixtures/lke_clusters_18882_pools_789.json | 1 + test/integration/models/lke/test_lke.py | 10 ++++++++-- test/unit/objects/lke_test.py | 4 ++++ 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/linode_api4/objects/lke.py b/linode_api4/objects/lke.py index 792aed988..0864052f1 100644 --- a/linode_api4/objects/lke.py +++ b/linode_api4/objects/lke.py @@ -205,6 +205,7 @@ class LKENodePool(DerivedBase): # directly exposed in the node pool response. "k8s_version": Property(mutable=True), "update_strategy": Property(mutable=True), + "firewall_id": Property(mutable=True), } def _parse_raw_node( diff --git a/test/fixtures/lke_clusters_18881_pools_456.json b/test/fixtures/lke_clusters_18881_pools_456.json index 9aa5fb0f0..7bf68a6f8 100644 --- a/test/fixtures/lke_clusters_18881_pools_456.json +++ b/test/fixtures/lke_clusters_18881_pools_456.json @@ -35,6 +35,7 @@ "bar": "foo" }, "label": "example-node-pool", + "firewall_id": 456, "type": "g6-standard-4", "disk_encryption": "enabled" } \ No newline at end of file diff --git a/test/fixtures/lke_clusters_18882_pools_789.json b/test/fixtures/lke_clusters_18882_pools_789.json index d3c17eedb..8a5ba21d8 100644 --- a/test/fixtures/lke_clusters_18882_pools_789.json +++ b/test/fixtures/lke_clusters_18882_pools_789.json @@ -15,5 +15,6 @@ "tags": [], "disk_encryption": "enabled", "k8s_version": "1.31.1+lke1", + "firewall_id": 789, "update_strategy": "rolling_update" } \ No newline at end of file diff --git a/test/integration/models/lke/test_lke.py b/test/integration/models/lke/test_lke.py index 241117442..71ebc1ff2 100644 --- a/test/integration/models/lke/test_lke.py +++ b/test/integration/models/lke/test_lke.py @@ -138,7 +138,7 @@ def lke_cluster_with_apl(test_linode_client): @pytest.fixture(scope="session") -def lke_cluster_enterprise(test_linode_client): +def lke_cluster_enterprise(e2e_test_firewall, test_linode_client): # We use the oldest version here so we can test upgrades version = sorted( v.id for v in test_linode_client.lke.tier("enterprise").versions() @@ -153,6 +153,7 @@ def lke_cluster_enterprise(test_linode_client): 3, k8s_version=version, update_strategy="rolling_update", + firewall_id=e2e_test_firewall.id, ) label = get_test_label() + "_cluster" @@ -434,13 +435,18 @@ def test_lke_cluster_with_apl(lke_cluster_with_apl): ) -def test_lke_cluster_enterprise(test_linode_client, lke_cluster_enterprise): +def test_lke_cluster_enterprise( + e2e_test_firewall, + test_linode_client, + lke_cluster_enterprise, +): lke_cluster_enterprise.invalidate() assert lke_cluster_enterprise.tier == "enterprise" pool = lke_cluster_enterprise.pools[0] assert str(pool.k8s_version) == lke_cluster_enterprise.k8s_version.id assert pool.update_strategy == "rolling_update" + assert pool.firewall_id == e2e_test_firewall.id target_version = sorted( v.id for v in test_linode_client.lke.tier("enterprise").versions() diff --git a/test/unit/objects/lke_test.py b/test/unit/objects/lke_test.py index cb9589cfb..10284a0c9 100644 --- a/test/unit/objects/lke_test.py +++ b/test/unit/objects/lke_test.py @@ -52,6 +52,7 @@ def test_get_pool(self): assert pool.cluster_id == 18881 assert pool.type.id == "g6-standard-4" assert pool.label == "example-node-pool" + assert pool.firewall_id == 456 assert pool.disk_encryption == InstanceDiskEncryptionType.enabled assert pool.disks is not None @@ -254,6 +255,7 @@ def test_lke_node_pool_update(self): pool.tags = ["foobar"] pool.count = 5 pool.label = "testing-label" + pool.firewall_id = 852 pool.autoscaler = { "enabled": True, "min": 2, @@ -281,6 +283,7 @@ def test_lke_node_pool_update(self): "labels": { "updated-key": "updated-value", }, + "firewall_id": 852, "taints": [ { "key": "updated-key", @@ -551,6 +554,7 @@ def test_cluster_enterprise(self): assert pool.k8s_version == "1.31.1+lke1" assert pool.update_strategy == "rolling_update" assert pool.label == "enterprise-node-pool" + assert pool.firewall_id == 789 def test_lke_tiered_version(self): version = TieredKubeVersion(self.client, "1.32", "standard")