From c29a0605bd934eaa6c3d2360e05e80441ef22766 Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Wed, 20 Aug 2025 13:49:28 +0530 Subject: [PATCH 1/9] adding monitors in region endpoint --- linode_api4/objects/monitor.py | 32 +++++++++++++++++- linode_api4/objects/region.py | 19 +++++++++++ test/fixtures/monitor_dashboards.json | 8 +++-- test/fixtures/monitor_dashboards_1.json | 8 +++-- .../monitor_services_dbaas_dashboards.json | 15 +++++++-- test/fixtures/regions.json | 8 +++++ test/unit/objects/monitor_test.py | 33 ++++++++++++++++--- test/unit/objects/region_test.py | 5 +++ 8 files changed, 117 insertions(+), 11 deletions(-) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index ed6ce79a5..f44db6e29 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -49,6 +49,7 @@ class ServiceType(StrEnum): firewall = "firewall" object_storage = "object_storage" aclb = "aclb" + netloadbalancer = "netloadbalancer" class MetricType(StrEnum): @@ -82,6 +83,10 @@ class MetricUnit(StrEnum): RATIO = "ratio" OPS_PER_SECOND = "ops_per_second" IOPS = "iops" + KILO_BYTES_PER_SECOND = "kilo_bytes_per_second" + SESSIONS_PER_SECOND = "sessions_per_second" + PACKETS_PER_SECOND = "packets_per_second" + KILO_BITS_PER_SECOND = "kilo_bits_per_second" class DashboardType(StrEnum): @@ -93,6 +98,17 @@ class DashboardType(StrEnum): custom = "custom" +@dataclass +class Filter(JSONObject): + """ + Represents a filter in the filters list of a dashboard widget. + """ + + dimension_label: str = "" + operator: str = "" + value: str = "" + + @dataclass class DashboardWidget(JSONObject): """ @@ -107,6 +123,19 @@ class DashboardWidget(JSONObject): chart_type: ChartType = "" y_label: str = "" aggregate_function: AggregateFunction = "" + group_by: List[str] = field(default_factory=list) + filters: Optional[List[Filter]] = None + + +@dataclass +class ServiceAlert(JSONObject): + """ + Represents alert configuration options for a monitor service. + """ + + polling_interval_seconds: List[int] = field(default_factory=list) + evaluation_period_seconds: List[int] = field(default_factory=list) + scope: List[str] = field(default_factory=list) @dataclass @@ -154,7 +183,7 @@ class MonitorDashboard(Base): "label": Property(), "service_type": Property(ServiceType), "type": Property(DashboardType), - "widgets": Property(List[DashboardWidget]), + "widgets": Property(json_object=DashboardWidget), "updated": Property(is_datetime=True), } @@ -171,6 +200,7 @@ class MonitorService(Base): properties = { "service_type": Property(ServiceType), "label": Property(), + "alert": Property(json_object=ServiceAlert), } diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index 34577c336..9e300b457 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -16,6 +16,24 @@ class RegionPlacementGroupLimits(JSONObject): maximum_linodes_per_pg: int = 0 +@dataclass +class RegionMonitors(JSONObject): + """ + Represents the monitor services available in a region. + Lists the services in this region that support metrics and alerts + use with Akamai Cloud Pulse (ACLP). + """ + + alerts: list[str] | None = None + metrics: list[str] | None = None + + def __post_init__(self): + if self.alerts is None: + self.alerts = [] + if self.metrics is None: + self.metrics = [] + + class Region(Base): """ A Region. Regions correspond to individual data centers, each located in a different geographical area. @@ -35,6 +53,7 @@ class Region(Base): "placement_group_limits": Property( json_object=RegionPlacementGroupLimits ), + "monitors": Property(json_object=RegionMonitors), } @property diff --git a/test/fixtures/monitor_dashboards.json b/test/fixtures/monitor_dashboards.json index 42de92b55..5e56923a1 100644 --- a/test/fixtures/monitor_dashboards.json +++ b/test/fixtures/monitor_dashboards.json @@ -16,7 +16,9 @@ "metric": "cpu_usage", "size": 12, "unit": "%", - "y_label": "cpu_usage" + "y_label": "cpu_usage", + "group_by": ["entity_id"], + "filters": null }, { "aggregate_function": "sum", @@ -26,7 +28,9 @@ "metric": "write_iops", "size": 6, "unit": "IOPS", - "y_label": "write_iops" + "y_label": "write_iops", + "group_by": ["entity_id"], + "filters": null } ] } diff --git a/test/fixtures/monitor_dashboards_1.json b/test/fixtures/monitor_dashboards_1.json index b78bf3447..afb5d71ee 100644 --- a/test/fixtures/monitor_dashboards_1.json +++ b/test/fixtures/monitor_dashboards_1.json @@ -14,7 +14,9 @@ "metric": "cpu_usage", "size": 12, "unit": "%", - "y_label": "cpu_usage" + "y_label": "cpu_usage", + "group_by": ["entity_id"], + "filters": null }, { "aggregate_function": "sum", @@ -24,7 +26,9 @@ "metric": "available_memory", "size": 6, "unit": "GB", - "y_label": "available_memory" + "y_label": "available_memory", + "group_by": ["entity_id"], + "filters": null } ] } \ No newline at end of file diff --git a/test/fixtures/monitor_services_dbaas_dashboards.json b/test/fixtures/monitor_services_dbaas_dashboards.json index 5fbb7e9db..e39a231b2 100644 --- a/test/fixtures/monitor_services_dbaas_dashboards.json +++ b/test/fixtures/monitor_services_dbaas_dashboards.json @@ -16,7 +16,9 @@ "metric": "cpu_usage", "size": 12, "unit": "%", - "y_label": "cpu_usage" + "y_label": "cpu_usage", + "group_by": ["entity_id"], + "filters": null }, { "aggregate_function": "sum", @@ -26,7 +28,16 @@ "metric": "memory_usage", "size": 6, "unit": "%", - "y_label": "memory_usage" + "y_label": "memory_usage", + "group_by": ["entity_id"], + "filters": [ + { + "dimension_label": "pattern", + "operator": "in", + "value": "publicout,privateout" + } + ] + } ] } diff --git a/test/fixtures/regions.json b/test/fixtures/regions.json index 5fe55e200..c3f192d47 100644 --- a/test/fixtures/regions.json +++ b/test/fixtures/regions.json @@ -125,6 +125,14 @@ "Block Storage", "Object Storage" ], + "monitors": { + "alerts": [ + "Managed Databases" + ], + "metrics": [ + "Managed Databases" + ] + }, "status": "ok", "resolvers": { "ipv4": "66.228.42.5,96.126.106.5,50.116.53.5,50.116.58.5,50.116.61.5,50.116.62.5,66.175.211.5,97.107.133.4,207.192.69.4,207.192.69.5", diff --git a/test/unit/objects/monitor_test.py b/test/unit/objects/monitor_test.py index a010514c2..86f39a6ca 100644 --- a/test/unit/objects/monitor_test.py +++ b/test/unit/objects/monitor_test.py @@ -41,6 +41,8 @@ def test_dashboard_by_ID(self): self.assertEqual(dashboard.widgets[0].size, 12) self.assertEqual(dashboard.widgets[0].unit, "%") self.assertEqual(dashboard.widgets[0].y_label, "cpu_usage") + self.assertEqual(dashboard.widgets[0].group_by, ["entity_id"]) + self.assertIsNone(dashboard.widgets[0].filters) def test_dashboard_by_service_type(self): dashboards = self.client.monitor.dashboards(service_type="dbaas") @@ -62,6 +64,21 @@ def test_dashboard_by_service_type(self): self.assertEqual(dashboards[0].widgets[0].size, 12) self.assertEqual(dashboards[0].widgets[0].unit, "%") self.assertEqual(dashboards[0].widgets[0].y_label, "cpu_usage") + self.assertEqual(dashboards[0].widgets[0].group_by, ["entity_id"]) + self.assertIsNone(dashboards[0].widgets[0].filters) + + # Test the second widget which has filters + self.assertEqual(dashboards[0].widgets[1].label, "Memory Usage") + self.assertEqual(dashboards[0].widgets[1].group_by, ["entity_id"]) + self.assertIsNotNone(dashboards[0].widgets[1].filters) + self.assertEqual(len(dashboards[0].widgets[1].filters), 1) + self.assertEqual( + dashboards[0].widgets[1].filters[0].dimension_label, "pattern" + ) + self.assertEqual(dashboards[0].widgets[1].filters[0].operator, "in") + self.assertEqual( + dashboards[0].widgets[1].filters[0].value, "publicout,privateout" + ) def test_get_all_dashboards(self): dashboards = self.client.monitor.dashboards() @@ -83,12 +100,20 @@ def test_get_all_dashboards(self): self.assertEqual(dashboards[0].widgets[0].size, 12) self.assertEqual(dashboards[0].widgets[0].unit, "%") self.assertEqual(dashboards[0].widgets[0].y_label, "cpu_usage") + self.assertEqual(dashboards[0].widgets[0].group_by, ["entity_id"]) + self.assertIsNone(dashboards[0].widgets[0].filters) def test_specific_service_details(self): data = self.client.load(MonitorService, "dbaas") self.assertEqual(data.label, "Databases") self.assertEqual(data.service_type, "dbaas") + # Test alert configuration + self.assertIsNotNone(data.alert) + self.assertEqual(data.alert.polling_interval_seconds, [300]) + self.assertEqual(data.alert.evaluation_period_seconds, [300]) + self.assertEqual(data.alert.scope, ["entity"]) + def test_metric_definitions(self): metrics = self.client.monitor.metric_definitions(service_type="dbaas") @@ -96,16 +121,16 @@ def test_metric_definitions(self): metrics[0].available_aggregate_functions, ["max", "avg", "min", "sum"], ) - self.assertEqual(metrics[0].is_alertable, True) + self.assertTrue(metrics[0].is_alertable) self.assertEqual(metrics[0].label, "CPU Usage") self.assertEqual(metrics[0].metric, "cpu_usage") self.assertEqual(metrics[0].metric_type, "gauge") self.assertEqual(metrics[0].scrape_interval, "60s") self.assertEqual(metrics[0].unit, "percent") - self.assertEqual(metrics[0].dimensions[0].dimension_label, "node_type") - self.assertEqual(metrics[0].dimensions[0].label, "Node Type") + self.assertEqual(metrics[0].dimensions[0]["dimension_label"], "node_type") + self.assertEqual(metrics[0].dimensions[0]["label"], "Node Type") self.assertEqual( - metrics[0].dimensions[0].values, ["primary", "secondary"] + metrics[0].dimensions[0]["values"], ["primary", "secondary"] ) def test_create_token(self): diff --git a/test/unit/objects/region_test.py b/test/unit/objects/region_test.py index 0bc1afa9e..f49bf2cc2 100644 --- a/test/unit/objects/region_test.py +++ b/test/unit/objects/region_test.py @@ -28,6 +28,11 @@ def test_get_region(self): region.placement_group_limits.maximum_linodes_per_pg, 5 ) + # Test monitors section + self.assertIsNotNone(region.monitors) + self.assertEqual(region.monitors.alerts, ["Managed Databases"]) + self.assertEqual(region.monitors.metrics, ["Managed Databases"]) + def test_region_availability(self): """ Tests that availability for a specific region can be listed and filtered on. From 487bce6facb5905d618904ae777b351083293bf8 Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Tue, 16 Sep 2025 16:56:56 +0530 Subject: [PATCH 2/9] updating with review comments --- linode_api4/objects/monitor.py | 4 ++-- linode_api4/objects/region.py | 12 +++--------- test/unit/objects/monitor_test.py | 6 +++--- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index f44db6e29..ab42aa143 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -49,7 +49,7 @@ class ServiceType(StrEnum): firewall = "firewall" object_storage = "object_storage" aclb = "aclb" - netloadbalancer = "netloadbalancer" + net_load_balancer = "netloadbalancer" class MetricType(StrEnum): @@ -124,7 +124,7 @@ class DashboardWidget(JSONObject): y_label: str = "" aggregate_function: AggregateFunction = "" group_by: List[str] = field(default_factory=list) - filters: Optional[List[Filter]] = None + filters: List[Filter] = field(default_factory=list) @dataclass diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index 9e300b457..bf3fa6ec4 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional from linode_api4.errors import UnexpectedResponseError @@ -24,14 +24,8 @@ class RegionMonitors(JSONObject): use with Akamai Cloud Pulse (ACLP). """ - alerts: list[str] | None = None - metrics: list[str] | None = None - - def __post_init__(self): - if self.alerts is None: - self.alerts = [] - if self.metrics is None: - self.metrics = [] + alerts: list[str] = field(default_factory=list) + metrics: list[str] = field(default_factory=list) class Region(Base): diff --git a/test/unit/objects/monitor_test.py b/test/unit/objects/monitor_test.py index 86f39a6ca..329a09063 100644 --- a/test/unit/objects/monitor_test.py +++ b/test/unit/objects/monitor_test.py @@ -127,10 +127,10 @@ def test_metric_definitions(self): self.assertEqual(metrics[0].metric_type, "gauge") self.assertEqual(metrics[0].scrape_interval, "60s") self.assertEqual(metrics[0].unit, "percent") - self.assertEqual(metrics[0].dimensions[0]["dimension_label"], "node_type") - self.assertEqual(metrics[0].dimensions[0]["label"], "Node Type") + self.assertEqual(metrics[0].dimensions[0].dimension_label, "node_type") + self.assertEqual(metrics[0].dimensions[0].label, "Node Type") self.assertEqual( - metrics[0].dimensions[0]["values"], ["primary", "secondary"] + metrics[0].dimensions[0].values, ["primary", "secondary"] ) def test_create_token(self): From 7b9bc6cfe469627408c31aa34bac8754256ab5ba Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Wed, 24 Sep 2025 13:38:13 +0530 Subject: [PATCH 3/9] fixing build issues --- linode_api4/objects/monitor.py | 16 +++++++--------- linode_api4/objects/region.py | 6 +++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index ab42aa143..9d107389e 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -5,7 +5,7 @@ "MonitorServiceToken", "AggregateFunction", ] -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import List, Optional from linode_api4.objects.base import Base, Property @@ -123,8 +123,8 @@ class DashboardWidget(JSONObject): chart_type: ChartType = "" y_label: str = "" aggregate_function: AggregateFunction = "" - group_by: List[str] = field(default_factory=list) - filters: List[Filter] = field(default_factory=list) + group_by: Optional[List[str]] = None + filters: Optional[List[Filter]] = None @dataclass @@ -133,9 +133,9 @@ class ServiceAlert(JSONObject): Represents alert configuration options for a monitor service. """ - polling_interval_seconds: List[int] = field(default_factory=list) - evaluation_period_seconds: List[int] = field(default_factory=list) - scope: List[str] = field(default_factory=list) + polling_interval_seconds: Optional[List[int]] = None + evaluation_period_seconds: Optional[List[int]] = None + scope: Optional[List[str]] = None @dataclass @@ -164,9 +164,7 @@ class MonitorMetricsDefinition(JSONObject): scrape_interval: int = 0 is_alertable: bool = False dimensions: Optional[List[Dimension]] = None - available_aggregate_functions: List[AggregateFunction] = field( - default_factory=list - ) + available_aggregate_functions: Optional[List[AggregateFunction]] = None class MonitorDashboard(Base): diff --git a/linode_api4/objects/region.py b/linode_api4/objects/region.py index bf3fa6ec4..c9dc05099 100644 --- a/linode_api4/objects/region.py +++ b/linode_api4/objects/region.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import List, Optional from linode_api4.errors import UnexpectedResponseError @@ -24,8 +24,8 @@ class RegionMonitors(JSONObject): use with Akamai Cloud Pulse (ACLP). """ - alerts: list[str] = field(default_factory=list) - metrics: list[str] = field(default_factory=list) + alerts: Optional[list[str]] = None + metrics: Optional[list[str]] = None class Region(Base): From 90518a9f67b146c8034a32ae98389da42b91670b Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Mon, 6 Oct 2025 19:23:19 +0530 Subject: [PATCH 4/9] fixing build issues --- linode_api4/objects/monitor.py | 6 +++--- test/unit/objects/region_test.py | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index 9d107389e..a499a55e8 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -5,7 +5,7 @@ "MonitorServiceToken", "AggregateFunction", ] -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional from linode_api4.objects.base import Base, Property @@ -123,8 +123,8 @@ class DashboardWidget(JSONObject): chart_type: ChartType = "" y_label: str = "" aggregate_function: AggregateFunction = "" - group_by: Optional[List[str]] = None - filters: Optional[List[Filter]] = None + group_by: List[str] = field(default_factory=list) + filters: List[Filter] = field(default_factory=list) @dataclass diff --git a/test/unit/objects/region_test.py b/test/unit/objects/region_test.py index a076ea386..73fdc8f5d 100644 --- a/test/unit/objects/region_test.py +++ b/test/unit/objects/region_test.py @@ -27,7 +27,6 @@ def test_get_region(self): region.placement_group_limits.maximum_linodes_per_pg, 5 ) - # Test monitors section self.assertIsNotNone(region.monitors) self.assertEqual(region.monitors.alerts, ["Managed Databases"]) @@ -36,7 +35,6 @@ def test_get_region(self): self.assertIsNotNone(region.capabilities) self.assertIn("Linode Interfaces", region.capabilities) - def test_region_availability(self): """ Tests that availability for a specific region can be listed and filtered on. From 126a93ce73fe474fbe5c74f8b373003e9561e0e6 Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Mon, 6 Oct 2025 21:31:24 +0530 Subject: [PATCH 5/9] fixing build issue --- linode_api4/objects/monitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index a499a55e8..b85af716f 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -123,8 +123,8 @@ class DashboardWidget(JSONObject): chart_type: ChartType = "" y_label: str = "" aggregate_function: AggregateFunction = "" - group_by: List[str] = field(default_factory=list) - filters: List[Filter] = field(default_factory=list) + group_by: List[str] = None + filters: List[Filter] = None @dataclass From c764906ef9095c6fd3a5a103d28287d2c13b88cd Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Mon, 6 Oct 2025 21:33:39 +0530 Subject: [PATCH 6/9] fixing build issue --- linode_api4/objects/monitor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index b85af716f..d275c62e1 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -123,8 +123,8 @@ class DashboardWidget(JSONObject): chart_type: ChartType = "" y_label: str = "" aggregate_function: AggregateFunction = "" - group_by: List[str] = None - filters: List[Filter] = None + group_by: Optional[List[str]] = None + filters: Optional[List[Filter]] = None @dataclass From a45a0fa2719ca4b74e3c600d343faa56ef9fe820 Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Wed, 8 Oct 2025 16:42:22 +0530 Subject: [PATCH 7/9] fixing lint --- linode_api4/objects/monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index d275c62e1..9d107389e 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -5,7 +5,7 @@ "MonitorServiceToken", "AggregateFunction", ] -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import List, Optional from linode_api4.objects.base import Base, Property From 8bd812fc65a96bd8ab406e7330e7ebecda0d04ea Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Tue, 14 Oct 2025 15:21:16 +0530 Subject: [PATCH 8/9] fixing filters --- linode_api4/objects/monitor.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/linode_api4/objects/monitor.py b/linode_api4/objects/monitor.py index 9d107389e..fb339a0fd 100644 --- a/linode_api4/objects/monitor.py +++ b/linode_api4/objects/monitor.py @@ -5,7 +5,7 @@ "MonitorServiceToken", "AggregateFunction", ] -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List, Optional from linode_api4.objects.base import Base, Property @@ -124,7 +124,22 @@ class DashboardWidget(JSONObject): y_label: str = "" aggregate_function: AggregateFunction = "" group_by: Optional[List[str]] = None - filters: Optional[List[Filter]] = None + _filters: Optional[List[Filter]] = field( + default=None, metadata={"json_key": "filters"} + ) + + def __getattribute__(self, name): + """Override to handle the filters attribute specifically to avoid metaclass conflict.""" + if name == "filters": + return object.__getattribute__(self, "_filters") + return object.__getattribute__(self, name) + + def __setattr__(self, name, value): + """Override to handle setting the filters attribute.""" + if name == "filters": + object.__setattr__(self, "_filters", value) + else: + object.__setattr__(self, name, value) @dataclass From fa0103045e4716fc3ae09b221b317d64c275e2d4 Mon Sep 17 00:00:00 2001 From: Priya Majali Date: Thu, 16 Oct 2025 13:28:45 +0530 Subject: [PATCH 9/9] adding int-test for filter and groupby --- .../models/monitor/test_monitor.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/integration/models/monitor/test_monitor.py b/test/integration/models/monitor/test_monitor.py index 7c9249f42..bc996f7be 100644 --- a/test/integration/models/monitor/test_monitor.py +++ b/test/integration/models/monitor/test_monitor.py @@ -35,6 +35,57 @@ def test_get_all_dashboards(test_linode_client): assert dashboards_by_svc[0].service_type == get_service_type +def test_filter_and_group_by(test_linode_client): + client = test_linode_client + dashboards_by_svc = client.monitor.dashboards(service_type="linode") + assert isinstance(dashboards_by_svc[0], MonitorDashboard) + + # Get the first dashboard for linode service type + dashboard = dashboards_by_svc[0] + assert dashboard.service_type == "linode" + + # Ensure the dashboard has widgets + assert hasattr( + dashboard, "widgets" + ), "Dashboard should have widgets attribute" + assert dashboard.widgets is not None, "Dashboard widgets should not be None" + assert ( + len(dashboard.widgets) > 0 + ), "Dashboard should have at least one widget" + + # Test the first widget's group_by and filters fields + widget = dashboard.widgets[0] + + # Test group_by field type + group_by = widget.group_by + assert group_by is None or isinstance( + group_by, list + ), "group_by should be None or list type" + if group_by is not None: + for item in group_by: + assert isinstance(item, str), "group_by items should be strings" + + # Test filters field type + filters = widget.filters + assert filters is None or isinstance( + filters, list + ), "filters should be None or list type" + if filters is not None: + from linode_api4.objects.monitor import Filter + + for filter_item in filters: + assert isinstance( + filter_item, Filter + ), "filter items should be Filter objects" + assert hasattr( + filter_item, "dimension_label" + ), "Filter should have dimension_label" + assert hasattr( + filter_item, "operator" + ), "Filter should have operator" + assert hasattr(filter_item, "value"), "Filter should have value" + + # List supported services def test_get_supported_services(test_linode_client): client = test_linode_client