From 8ce3c5b8c8d041971c87a9be75823f6d57c163b2 Mon Sep 17 00:00:00 2001 From: Andreas Motl Date: Fri, 29 Mar 2024 14:40:16 +0100 Subject: [PATCH] Add support for Grafana library elements (#168) Co-authored-by: David Ameiss --- README.md | 1 + grafana_client/api.py | 2 + grafana_client/elements/__init__.py | 2 + grafana_client/elements/_async/__init__.py | 2 + .../elements/_async/libraryelement.py | 198 +++ grafana_client/elements/libraryelement.py | 198 +++ grafana_client/knowledge.py | 5 +- test/elements/test_libraryelement.py | 1088 +++++++++++++++++ test/test_grafana_client.py | 2 +- 9 files changed, 1493 insertions(+), 5 deletions(-) create mode 100644 grafana_client/elements/_async/libraryelement.py create mode 100644 grafana_client/elements/libraryelement.py create mode 100644 test/elements/test_libraryelement.py diff --git a/README.md b/README.md index 816224f..c6b31e2 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,7 @@ versions of Grafana might not support certain features or subsystems. | Folder Permissions | + | | Folder/Dashboard Search | +- | | Health | + | +| Library Elements | + | | Organisation | + | | Other | + | | Plugin | + | diff --git a/grafana_client/api.py b/grafana_client/api.py index 15ff07b..f9317cb 100644 --- a/grafana_client/api.py +++ b/grafana_client/api.py @@ -19,6 +19,7 @@ Datasource, Folder, Health, + LibraryElement, Notifications, Organization, Organizations, @@ -103,6 +104,7 @@ def __init__( self.notifications = Notifications(self.client) self.plugin = Plugin(self.client) self.serviceaccount = ServiceAccount(self.client) + self.libraryelement = LibraryElement(self.client, self) self._grafana_info = None def connect(self): diff --git a/grafana_client/elements/__init__.py b/grafana_client/elements/__init__.py index bf98122..e50b5e0 100644 --- a/grafana_client/elements/__init__.py +++ b/grafana_client/elements/__init__.py @@ -8,6 +8,7 @@ from .datasource import Datasource from .folder import Folder from .health import Health +from .libraryelement import LibraryElement from .notifications import Notifications from .organization import Organization, Organizations from .plugin import Plugin @@ -29,6 +30,7 @@ "Datasource", "Folder", "Health", + "LibraryElement", "Notifications", "Organization", "Organizations", diff --git a/grafana_client/elements/_async/__init__.py b/grafana_client/elements/_async/__init__.py index 1fb9500..a8f6ab4 100644 --- a/grafana_client/elements/_async/__init__.py +++ b/grafana_client/elements/_async/__init__.py @@ -7,6 +7,7 @@ from .datasource import Datasource as AsyncDatasource from .folder import Folder as AsyncFolder from .health import Health as AsyncHealth +from .libraryelement import LibraryElement as AsyncLibraryElement from .notifications import Notifications as AsyncNotifications from .organization import Organization as AsyncOrganization from .organization import Organizations as AsyncOrganizations @@ -29,6 +30,7 @@ "AsyncDatasource", "AsyncFolder", "AsyncHealth", + "AsyncLibraryElement", "AsyncNotifications", "AsyncOrganization", "AsyncOrganizations", diff --git a/grafana_client/elements/_async/libraryelement.py b/grafana_client/elements/_async/libraryelement.py new file mode 100644 index 0000000..b4b104c --- /dev/null +++ b/grafana_client/elements/_async/libraryelement.py @@ -0,0 +1,198 @@ +from verlib2 import Version + +from ..base import Base + +VERSION_8_2 = Version("8.2") + + +class LibraryElement(Base): + Panel: int = 1 + Variable: int = 2 + + def __init__(self, client, api): + super(LibraryElement, self).__init__(client) + self.client = client + self.api = api + + async def get_library_element(self, element_uid: str) -> any: + """ + + :param element_uid: + :return: + """ + if await self.api.version and Version(await self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + get_element_path = f"/library-elements/{element_uid}" + return await self.client.GET(get_element_path) + + async def get_library_element_by_name(self, element_name: str) -> any: + """ + + :param element_name: + :return: + """ + if await self.api.version and Version(await self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + get_element_path = f"/library-elements/name/{element_name}" + return await self.client.GET(get_element_path) + + async def get_library_element_connections(self, element_uid: str) -> any: + """ + + :param element_uid: + :return: + """ + if await self.api.version and Version(await self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + get_element_connections_path = f"/library-elements/{element_uid}/connections" + return await self.client.GET(get_element_connections_path) + + async def create_library_element( + self, model: dict, name: str = None, kind: int = Panel, uid: str = None, folder_uid: str = None + ): + """ + + :param model: + :param name: + :param kind: + :param uid: + :param folder_uid: + :return: + """ + if await self.api.version and Version(await self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + json: dict = dict() + # If the model contains a "meta" entry, use the "folderUid" entry if folder_uid isn't given + if folder_uid is not None: + json["folderUid"] = folder_uid + elif "meta" in model and "folderUid" in model["meta"]: + json["folderUid"] = model["meta"]["folderUid"] + # If the model contains a "model" entry, use the "uid" entry if uid isn't given + if uid is not None: + json["uid"] = uid + elif "model" in model and "uid" in model["model"]: + json["uid"] = model["model"]["uid"] + # If the model contains a "model" entry, use the "title" entry if title isn't given + if name is not None: + json["name"] = name + elif "model" in model and "title" in model["model"]: + json["name"] = model["model"]["title"] + if "model" in model: + json["model"] = model["model"] + else: + json["model"] = model + # If the model contains a "kind" entry, use that instead of the specified kind + if "kind" in model: + json["kind"] = model["kind"] + else: + json["kind"] = kind + create_element_path = "/library-elements" + return await self.client.POST(create_element_path, json=json) + + async def update_library_element( + self, uid: str, model: dict, name: str = None, kind: int = Panel, folder_uid: str = None, version: int = None + ): + """ + + :param uid: + :param model: + :param name: + :param kind: + :param folder_uid: + :return: + """ + if await self.api.version and Version(await self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + json: dict = dict() + # If the model contains a "meta" entry, use the "folderUid" entry if folder_uid isn't given + if folder_uid is not None: + json["folderUid"] = folder_uid + elif "meta" in model and "folderUid" in model["meta"]: + json["folderUid"] = model["meta"]["folderUid"] + json["uid"] = uid + # If the model contains a "model" entry, use the "title" entry if title isn't given + if name is not None: + json["name"] = name + elif "model" in model and "title" in model["model"]: + json["name"] = model["model"]["title"] + if "model" in model: + json["model"] = model["model"] + else: + json["model"] = model + # If the model contains a "kind" entry, use that instead of the specified kind + if "kind" in model: + json["kind"] = model["kind"] + else: + json["kind"] = kind + if version is None: + current_element = await self.get_library_element(uid) + if "version" in current_element: + json["version"] = current_element["version"] + else: + raise ValueError("version must be specified") + else: + json["version"] = version + + update_element_path = f"/library-elements/{uid}" + return await self.client.PATCH(update_element_path, json=json) + + async def delete_library_element(self, element_uid: str) -> any: + """ + + :param element_uid: + :return: + """ + if await self.api.version and Version(await self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + delete_element_path = f"/library-elements/{element_uid}" + return await self.client.DELETE(delete_element_path) + + async def list_library_elements( + self, + search_string: str = None, + kind: int = None, + sort_direction: str = None, + type_filter: str = None, + exclude_uid: str = None, + folder_filter: str = None, + per_page: int = None, + page: int = None, + ) -> any: + """ + + :param search_string: + :param kind: + :param sort_direction: + :param type_filter: + :param exclude_uid: + :param folder_filter: + :param per_page: + :param page: + + :return: + """ + if await self.api.version and Version(await self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + list_elements_path = "/library-elements" + params = [] + + if search_string is not None: + params.append("searchString=%s" % search_string) + if kind is not None: + params.append("kind=%d" % kind) + if sort_direction is not None: + params.append("sortDirection=%s" % sort_direction) + if type_filter is not None: + params.append("typeFilter=%s" % type_filter) + if exclude_uid is not None: + params.append("excludeUid=%s" % exclude_uid) + if folder_filter is not None: + params.append("folderFilter=%s" % folder_filter) + if per_page is not None: + params.append("perPage=%d" % per_page) + if page is not None: + params.append("page=%d" % page) + + list_elements_path += "?" + list_elements_path += "&".join(params) + return await self.client.GET(list_elements_path) diff --git a/grafana_client/elements/libraryelement.py b/grafana_client/elements/libraryelement.py new file mode 100644 index 0000000..855a919 --- /dev/null +++ b/grafana_client/elements/libraryelement.py @@ -0,0 +1,198 @@ +from verlib2 import Version + +from .base import Base + +VERSION_8_2 = Version("8.2") + + +class LibraryElement(Base): + Panel: int = 1 + Variable: int = 2 + + def __init__(self, client, api): + super(LibraryElement, self).__init__(client) + self.client = client + self.api = api + + def get_library_element(self, element_uid: str) -> any: + """ + + :param element_uid: + :return: + """ + if self.api.version and Version(self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + get_element_path = f"/library-elements/{element_uid}" + return self.client.GET(get_element_path) + + def get_library_element_by_name(self, element_name: str) -> any: + """ + + :param element_name: + :return: + """ + if self.api.version and Version(self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + get_element_path = f"/library-elements/name/{element_name}" + return self.client.GET(get_element_path) + + def get_library_element_connections(self, element_uid: str) -> any: + """ + + :param element_uid: + :return: + """ + if self.api.version and Version(self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + get_element_connections_path = f"/library-elements/{element_uid}/connections" + return self.client.GET(get_element_connections_path) + + def create_library_element( + self, model: dict, name: str = None, kind: int = Panel, uid: str = None, folder_uid: str = None + ): + """ + + :param model: + :param name: + :param kind: + :param uid: + :param folder_uid: + :return: + """ + if self.api.version and Version(self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + json: dict = dict() + # If the model contains a "meta" entry, use the "folderUid" entry if folder_uid isn't given + if folder_uid is not None: + json["folderUid"] = folder_uid + elif "meta" in model and "folderUid" in model["meta"]: + json["folderUid"] = model["meta"]["folderUid"] + # If the model contains a "model" entry, use the "uid" entry if uid isn't given + if uid is not None: + json["uid"] = uid + elif "model" in model and "uid" in model["model"]: + json["uid"] = model["model"]["uid"] + # If the model contains a "model" entry, use the "title" entry if title isn't given + if name is not None: + json["name"] = name + elif "model" in model and "title" in model["model"]: + json["name"] = model["model"]["title"] + if "model" in model: + json["model"] = model["model"] + else: + json["model"] = model + # If the model contains a "kind" entry, use that instead of the specified kind + if "kind" in model: + json["kind"] = model["kind"] + else: + json["kind"] = kind + create_element_path = "/library-elements" + return self.client.POST(create_element_path, json=json) + + def update_library_element( + self, uid: str, model: dict, name: str = None, kind: int = Panel, folder_uid: str = None, version: int = None + ): + """ + + :param uid: + :param model: + :param name: + :param kind: + :param folder_uid: + :return: + """ + if self.api.version and Version(self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + json: dict = dict() + # If the model contains a "meta" entry, use the "folderUid" entry if folder_uid isn't given + if folder_uid is not None: + json["folderUid"] = folder_uid + elif "meta" in model and "folderUid" in model["meta"]: + json["folderUid"] = model["meta"]["folderUid"] + json["uid"] = uid + # If the model contains a "model" entry, use the "title" entry if title isn't given + if name is not None: + json["name"] = name + elif "model" in model and "title" in model["model"]: + json["name"] = model["model"]["title"] + if "model" in model: + json["model"] = model["model"] + else: + json["model"] = model + # If the model contains a "kind" entry, use that instead of the specified kind + if "kind" in model: + json["kind"] = model["kind"] + else: + json["kind"] = kind + if version is None: + current_element = self.get_library_element(uid) + if "version" in current_element: + json["version"] = current_element["version"] + else: + raise ValueError("version must be specified") + else: + json["version"] = version + + update_element_path = f"/library-elements/{uid}" + return self.client.PATCH(update_element_path, json=json) + + def delete_library_element(self, element_uid: str) -> any: + """ + + :param element_uid: + :return: + """ + if self.api.version and Version(self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + delete_element_path = f"/library-elements/{element_uid}" + return self.client.DELETE(delete_element_path) + + def list_library_elements( + self, + search_string: str = None, + kind: int = None, + sort_direction: str = None, + type_filter: str = None, + exclude_uid: str = None, + folder_filter: str = None, + per_page: int = None, + page: int = None, + ) -> any: + """ + + :param search_string: + :param kind: + :param sort_direction: + :param type_filter: + :param exclude_uid: + :param folder_filter: + :param per_page: + :param page: + + :return: + """ + if self.api.version and Version(self.api.version) < VERSION_8_2: + raise DeprecationWarning("Grafana versions earlier than 8.2 do not support library elements") + list_elements_path = "/library-elements" + params = [] + + if search_string is not None: + params.append("searchString=%s" % search_string) + if kind is not None: + params.append("kind=%d" % kind) + if sort_direction is not None: + params.append("sortDirection=%s" % sort_direction) + if type_filter is not None: + params.append("typeFilter=%s" % type_filter) + if exclude_uid is not None: + params.append("excludeUid=%s" % exclude_uid) + if folder_filter is not None: + params.append("folderFilter=%s" % folder_filter) + if per_page is not None: + params.append("perPage=%d" % per_page) + if page is not None: + params.append("page=%d" % page) + + list_elements_path += "?" + list_elements_path += "&".join(params) + return self.client.GET(list_elements_path) diff --git a/grafana_client/knowledge.py b/grafana_client/knowledge.py index 4885517..2170f1a 100644 --- a/grafana_client/knowledge.py +++ b/grafana_client/knowledge.py @@ -345,10 +345,7 @@ def query_factory(datasource, model: Optional[dict] = None, expression: Optional "name": "refId", "default": None, }, - { - "name": "builderOptions", - "default": None - } + {"name": "builderOptions", "default": None}, ] elif datasource_type == "prometheus": query = { diff --git a/test/elements/test_libraryelement.py b/test/elements/test_libraryelement.py new file mode 100644 index 0000000..9af7585 --- /dev/null +++ b/test/elements/test_libraryelement.py @@ -0,0 +1,1088 @@ +import unittest + +import requests +import requests_mock + +from grafana_client import GrafanaApi +from grafana_client.client import GrafanaBadInputError, GrafanaClientError + + +class LibraryElementTestCase(unittest.TestCase): + ConnectedPanelJSON: dict = { + "id": 2, + "kind": 1, + "meta": {"connectedDashboards": 2, "folderName": "Pulsar", "folderUid": "d6818acd-f7b1-433e-a679-7f206a7ce37a"}, + "model": { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 1}, + "id": 1, + "libraryPanel": {"name": "CPU Seconds", "uid": "cec85d6f-834b-427e-8993-562d34fff5c4"}, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'rate(process_cpu_seconds_total{job=~"$Job", instance=~"$Instance"}[$__rate_interval])', + "fullMetaSearch": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "__auto", + "range": True, + "refId": "A", + "useBackend": False, + } + ], + "title": "CPU Seconds", + "type": "timeseries", + }, + "name": "CPU Seconds", + "orgId": 1, + "type": "timeseries", + "uid": "cec85d6f-834b-427e-8993-562d34fff5c4", + "version": 1, + } + ConnectedPanelUID: str = "cec85d6f-834b-427e-8993-562d34fff5c4" + ConnectedPanelName: str = "CPU Seconds" + ConnectedPanelConnectionsJSON: dict = { + "result": [ + { + "id": 2, + "kind": 1, + "elementId": 2, + "connectionId": 101, + "connectionUid": "de3791ac-6079-4c18-bde0-cb390c079722", + "created": "2024-02-06T12:19:14-06:00", + "createdBy": {"avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", "id": 1, "name": "admin"}, + }, + { + "id": 5, + "kind": 1, + "elementId": 2, + "connectionId": 102, + "connectionUid": "a45fbfd0-b211-45fc-96ae-a56886075948", + "created": "2024-02-06T13:21:12-06:00", + "createdBy": {"avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", "id": 1, "name": "admin"}, + }, + ] + } + ConnectedPanelConnectionUIDs: list = ( + "de3791ac-6079-4c18-bde0-cb390c079722", + "a45fbfd0-b211-45fc-96ae-a56886075948", + ) + UnconnectedPanelJSON: dict = { + "id": 3, + "kind": 1, + "meta": { + "folderName": "Pulsar", + "folderUid": "d6818acd-f7b1-433e-a679-7f206a7ce37a", + }, + "model": { + "datasource": {"type": "prometheus", "uid": "fb5e0357-258c-4831-b447-565be35828b5"}, + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}, + "id": 3, + "libraryPanel": {"name": "Heap Memory", "uid": "b07d36c0-b5f6-4228-b0c0-d3c21e16a5f6"}, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "fb5e0357-258c-4831-b447-565be35828b5"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'jvm_memory_bytes_committed{job=~"$Job", instance=~"$Instance", area="heap"}', + "fullMetaSearch": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "Committed", + "range": True, + "refId": "A", + "useBackend": False, + }, + { + "datasource": {"type": "prometheus", "uid": "fb5e0357-258c-4831-b447-565be35828b5"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'jvm_memory_bytes_used{job=~"$Job", instance=~"$Instance", area="heap"}', + "fullMetaSearch": False, + "hide": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "Used", + "range": True, + "refId": "B", + "useBackend": False, + }, + { + "datasource": {"type": "prometheus", "uid": "fb5e0357-258c-4831-b447-565be35828b5"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'jvm_memory_bytes_max{job=~"$Job", instance=~"$Instance", area="heap"}', + "fullMetaSearch": False, + "hide": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "Max", + "range": True, + "refId": "C", + "useBackend": False, + }, + ], + "title": "Heap Memory", + "type": "timeseries", + }, + "name": "Heap Memory", + "orgId": 1, + "type": "timeseries", + "uid": "b07d36c0-b5f6-4228-b0c0-d3c21e16a5f6", + "version": 1, + } + UnconnectedPanelUID: str = "b07d36c0-b5f6-4228-b0c0-d3c21e16a5f6" + UnconnectedPanelName: str = "Heap Memory" + UnconnectedPanelConnectionsJSON: dict = {"result": []} + MissingPanelUID: str = "missing-panel" + MissingPanelName: str = "Unknown name" + + CreatePanelModelJSON: dict = { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "description": "", + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "axisBorderShow": False, + "axisCenteredZero": False, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": {"legend": False, "tooltip": False, "viz": False}, + "insertNulls": False, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": {"type": "linear"}, + "showPoints": "auto", + "spanNulls": False, + "stacking": {"group": "A", "mode": "none"}, + "thresholdsStyle": {"mode": "off"}, + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{"color": "green", "value": None}, {"color": "red", "value": 80}], + }, + "unit": "s", + }, + "overrides": [], + }, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 1}, + "libraryPanel": {"name": "CPU Seconds", "uid": "cec85d6f-834b-427e-8993-562d34fff5c4"}, + "options": { + "legend": {"calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": False}, + "tooltip": {"mode": "multi", "sort": "none"}, + }, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'rate(process_cpu_seconds_total{job=~"$Job", instance=~"$Instance"}[$__rate_interval])', + "fullMetaSearch": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "__auto", + "range": True, + "refId": "A", + "useBackend": False, + } + ], + "title": "CPU Seconds", + "transparent": True, + "type": "timeseries", + } + CreatePanelUID: str = "cec85d6f-834b-427e-8993-562d34fff5c4" + CreatePanelName: str = "CPU Seconds" + CreatePanelFolderUID: str = "d6818acd-f7b1-433e-a679-7f206a7ce37a" + CreatePanelResponseJSON: dict = { + "result": { + "id": 4, + "orgId": 1, + "folderId": 100, + "folderUid": "d6818acd-f7b1-433e-a679-7f206a7ce37a", + "uid": "cec85d6f-834b-427e-8993-562d34fff5c4", + "name": "CPU Seconds", + "kind": 1, + "type": "timeseries", + "description": "", + "model": { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "description": "", + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "axisBorderShow": False, + "axisCenteredZero": False, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": {"legend": False, "tooltip": False, "viz": False}, + "insertNulls": False, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": {"type": "linear"}, + "showPoints": "auto", + "spanNulls": False, + "stacking": {"group": "A", "mode": "none"}, + "thresholdsStyle": {"mode": "off"}, + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{"color": "green", "value": None}, {"color": "red", "value": 80}], + }, + "unit": "s", + }, + "overrides": [], + }, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 1}, + "libraryPanel": {"name": "CPU Seconds", "uid": "cec85d6f-834b-427e-8993-562d34fff5c4"}, + "options": { + "legend": {"calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": False}, + "tooltip": {"mode": "multi", "sort": "none"}, + }, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'rate(process_cpu_seconds_total{job=~"$Job", instance=~"$Instance"}[$__rate_interval])', + "fullMetaSearch": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "__auto", + "range": True, + "refId": "A", + "useBackend": False, + } + ], + "title": "CPU Seconds", + "transparent": True, + "type": "timeseries", + }, + "version": 1, + "meta": { + "folderName": "Pulsar", + "folderUid": "d6818acd-f7b1-433e-a679-7f206a7ce37a", + "connectedDashboards": 0, + "created": "2024-02-06T14:47:17.635073-06:00", + "updated": "2024-02-06T14:47:17.635073-06:00", + "createdBy": {"avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", "id": 1, "name": "admin"}, + "updatedBy": {"avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", "id": 1, "name": "admin"}, + }, + } + } + UpdatePanelModelJSON: dict = { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "gridPos": {"h": 8, "w": 12, "x": 10, "y": 15}, + "id": 1, + "libraryPanel": {"name": "CPU Seconds", "uid": "cec85d6f-834b-427e-8993-562d34fff5c4"}, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'rate(process_cpu_seconds_total{job=~"$Job", instance=~"$Instance"}[$__rate_interval])', + "fullMetaSearch": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "__auto", + "range": True, + "refId": "A", + "useBackend": False, + } + ], + "title": "CPU Seconds", + "type": "timeseries", + } + UpdatePanelResponseJSON: dict = { + "result": { + "id": 4, + "orgId": 1, + "folderId": 100, + "folderUid": "d6818acd-f7b1-433e-a679-7f206a7ce37a", + "uid": "cec85d6f-834b-427e-8993-562d34fff5c4", + "name": "CPU Seconds", + "kind": 1, + "type": "timeseries", + "description": "", + "model": { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "description": "", + "gridPos": {"h": 8, "w": 12, "x": 10, "y": 15}, + "id": 2, + "libraryPanel": {"name": "CPU Seconds", "uid": "cec85d6f-834b-427e-8993-562d34fff5c4"}, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'rate(process_cpu_seconds_total{job=~"$Job", instance=~"$Instance"}[$__rate_interval])', + "fullMetaSearch": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "__auto", + "range": True, + "refId": "A", + "useBackend": False, + } + ], + "title": "CPU Seconds", + "type": "timeseries", + }, + "version": 2, + "meta": { + "folderName": "Pulsar", + "folderUid": "d6818acd-f7b1-433e-a679-7f206a7ce37a", + "connectedDashboards": 0, + "created": "2024-02-06T14:47:17-06:00", + "updated": "2024-02-06T16:45:47.503116-06:00", + "createdBy": {"avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", "id": 1, "name": "admin"}, + "updatedBy": {"avatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", "id": 1, "name": "admin"}, + }, + } + } + UpdatePanelUID: str = "cec85d6f-834b-427e-8993-562d34fff5c4" + UpdatePanelName: str = "CPU Seconds" + UpdatePanelFolderUID: str = "d6818acd-f7b1-433e-a679-7f206a7ce37a" + ListLibraryElementsResponseJSON: dict = { + "result": { + "totalCount": 2, + "elements": [ + { + "id": 5, + "orgId": 1, + "folderId": 100, + "folderUid": "d6818acd-f7b1-433e-a679-7f206a7ce37a", + "uid": "cec85d6f-834b-427e-8993-562d34fff5c4", + "name": "CPU Seconds", + "kind": 1, + "type": "timeseries", + "description": "", + "model": { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "description": "", + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "axisBorderShow": False, + "axisCenteredZero": False, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": {"legend": False, "tooltip": False, "viz": False}, + "insertNulls": False, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": {"type": "linear"}, + "showPoints": "auto", + "spanNulls": False, + "stacking": {"group": "A", "mode": "none"}, + "thresholdsStyle": {"mode": "off"}, + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [{"color": "green", "value": None}, {"color": "red", "value": 80}], + }, + "unit": "s", + }, + "overrides": [], + }, + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 1}, + "id": 1, + "libraryPanel": {"name": "CPU Seconds", "uid": "cec85d6f-834b-427e-8993-562d34fff5c4"}, + "options": { + "legend": {"calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": False}, + "tooltip": {"mode": "multi", "sort": "none"}, + }, + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "d2caab40-4055-4236-a9b3-67ae334e096c"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'rate(process_cpu_seconds_total{job=~"$Job", instance=~"$Instance"}[$__rate_interval])', # noqa: E501 + "fullMetaSearch": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "__auto", + "range": True, + "refId": "A", + "useBackend": False, + } + ], + "title": "CPU Seconds", + "transparent": True, + "type": "timeseries", + }, + "version": 1, + "meta": { + "folderName": "Pulsar", + "folderUid": "d6818acd-f7b1-433e-a679-7f206a7ce37a", + "connectedDashboards": 0, + "created": "2024-02-07T10:25:19-06:00", + "updated": "2024-02-07T10:25:19-06:00", + "createdBy": { + "avatarUrl": "/avatar/3dbccb5e89c37491dae50c6a4be200c6", + "id": 2, + "name": "sa-tibco-messaging-monitoring", + }, + "updatedBy": { + "avatarUrl": "/avatar/3dbccb5e89c37491dae50c6a4be200c6", + "id": 2, + "name": "sa-tibco-messaging-monitoring", + }, + }, + }, + { + "id": 6, + "orgId": 1, + "folderId": 100, + "folderUid": "d6818acd-f7b1-433e-a679-7f206a7ce37a", + "uid": "b07d36c0-b5f6-4228-b0c0-d3c21e16a5f6", + "name": "Heap Memory", + "kind": 1, + "type": "timeseries", + "description": "", + "model": { + "datasource": {"type": "prometheus", "uid": "fb5e0357-258c-4831-b447-565be35828b5"}, + "description": "", + "fieldConfig": { + "defaults": { + "color": {"mode": "palette-classic"}, + "custom": { + "axisBorderShow": False, + "axisCenteredZero": False, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": {"legend": False, "tooltip": False, "viz": False}, + "insertNulls": False, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": {"type": "linear"}, + "showPoints": "never", + "spanNulls": False, + "stacking": {"group": "A", "mode": "none"}, + "thresholdsStyle": {"mode": "off"}, + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [{"color": "green", "value": None}, {"color": "red", "value": 80}], + }, + "unit": "decbytes", + }, + "overrides": [], + }, + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 0}, + "id": 3, + "interval": "1m", + "libraryPanel": {"name": "Heap Memory", "uid": "b07d36c0-b5f6-4228-b0c0-d3c21e16a5f6"}, + "links": [], + "options": { + "legend": { + "calcs": ["lastNotNull"], + "displayMode": "list", + "placement": "bottom", + "showLegend": False, + }, + "tooltip": {"mode": "multi", "sort": "none"}, + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": {"type": "prometheus", "uid": "fb5e0357-258c-4831-b447-565be35828b5"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'jvm_memory_bytes_committed{job=~"$Job", instance=~"$Instance", area="heap"}', + "fullMetaSearch": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "Committed", + "range": True, + "refId": "A", + "useBackend": False, + }, + { + "datasource": {"type": "prometheus", "uid": "fb5e0357-258c-4831-b447-565be35828b5"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'jvm_memory_bytes_used{job=~"$Job", instance=~"$Instance", area="heap"}', + "fullMetaSearch": False, + "hide": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "Used", + "range": True, + "refId": "B", + "useBackend": False, + }, + { + "datasource": {"type": "prometheus", "uid": "fb5e0357-258c-4831-b447-565be35828b5"}, + "disableTextWrap": False, + "editorMode": "builder", + "expr": 'jvm_memory_bytes_max{job=~"$Job", instance=~"$Instance", area="heap"}', + "fullMetaSearch": False, + "hide": False, + "includeNullMetadata": True, + "instant": False, + "legendFormat": "Max", + "range": True, + "refId": "C", + "useBackend": False, + }, + ], + "title": "Heap Memory", + "transparent": True, + "type": "timeseries", + }, + "version": 1, + "meta": { + "folderName": "Pulsar", + "folderUid": "d6818acd-f7b1-433e-a679-7f206a7ce37a", + "connectedDashboards": 0, + "created": "2024-02-07T10:25:37-06:00", + "updated": "2024-02-07T10:25:37-06:00", + "createdBy": { + "avatarUrl": "/avatar/3dbccb5e89c37491dae50c6a4be200c6", + "id": 2, + "name": "sa-tibco-messaging-monitoring", + }, + "updatedBy": { + "avatarUrl": "/avatar/3dbccb5e89c37491dae50c6a4be200c6", + "id": 2, + "name": "sa-tibco-messaging-monitoring", + }, + }, + }, + ], + "page": 1, + "perPage": 100, + } + } + + HealthResponsePre8_2: dict = {"commit": "unknown", "database": "ok", "version": "8.1.8"} + HealthResponsePost8_2: dict = {"commit": "unknown-dev", "database": "ok", "version": "10.2.2"} + + def setUp(self): + self.grafana = GrafanaApi(("admin", "admin"), host="localhost", url_path_prefix="", protocol="http") + + @requests_mock.Mocker() + def test_get_library_element(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.get( + f"http://localhost/api/library-elements/{LibraryElementTestCase.ConnectedPanelUID}", + json=LibraryElementTestCase.ConnectedPanelJSON, + ) + element = self.grafana.libraryelement.get_library_element(LibraryElementTestCase.ConnectedPanelUID) + self.assertEqual(element["uid"], LibraryElementTestCase.ConnectedPanelUID) + + @requests_mock.Mocker() + def test_get_library_element_grafana_pre_8_2(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.get( + f"http://localhost/api/library-elements/{LibraryElementTestCase.ConnectedPanelUID}", + json=LibraryElementTestCase.ConnectedPanelJSON, + ) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.get_library_element(LibraryElementTestCase.ConnectedPanelUID) + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) + + @requests_mock.Mocker() + def test_get_library_element_notfound(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.get( + f"http://localhost/api/library-elements/{LibraryElementTestCase.MissingPanelUID}", + json={"message": "library element could not be found"}, + status_code=404, + ) + with self.assertRaises(GrafanaClientError) as ex: + self.grafana.libraryelement.get_library_element(LibraryElementTestCase.MissingPanelUID) + self.assertEqual(404, ex.exception.status_code) + self.assertEqual("Client Error 404: library element could not be found", ex.exception.message) + + @requests_mock.Mocker() + def test_get_library_element_notfound_grafana_pre_8_2(self, m): + def custom_matcher(request): + if request.path_url == "/api/health": + return None + resp = requests.Response() + resp.status_code = 404 + resp._content = b'{"message": "library element could not be found"}' + resp.headers["Content-Type"] = "application/json" + return resp + + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.add_matcher(custom_matcher) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.get_library_element(LibraryElementTestCase.MissingPanelUID) + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) + + @requests_mock.Mocker() + def test_get_library_element_by_name(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.get( + f"http://localhost/api/library-elements/name/{LibraryElementTestCase.ConnectedPanelName}", + json=LibraryElementTestCase.ConnectedPanelJSON, + ) + element = self.grafana.libraryelement.get_library_element_by_name(LibraryElementTestCase.ConnectedPanelName) + self.assertEqual(element["name"], LibraryElementTestCase.ConnectedPanelName) + + @requests_mock.Mocker() + def test_get_library_element_by_name_grafana_pre_8_2(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.get( + f"http://localhost/api/library-elements/name/{LibraryElementTestCase.ConnectedPanelName}", + json=LibraryElementTestCase.ConnectedPanelJSON, + ) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.get_library_element_by_name(LibraryElementTestCase.ConnectedPanelName) + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) + + @requests_mock.Mocker() + def test_get_library_element_by_name_notfound(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.get( + f"http://localhost/api/library-elements/name/{LibraryElementTestCase.MissingPanelName}", + json={"message": "library element could not be found"}, + status_code=404, + ) + with self.assertRaises(GrafanaClientError) as ex: + self.grafana.libraryelement.get_library_element_by_name(LibraryElementTestCase.MissingPanelName) + self.assertEqual(404, ex.exception.status_code) + self.assertEqual("Client Error 404: library element could not be found", ex.exception.message) + + @requests_mock.Mocker() + def test_get_library_element_by_name_notfound_grafana_pre_8_2(self, m): + def custom_matcher(request): + if request.path_url == "/api/health": + return None + resp = requests.Response() + resp.status_code = 404 + resp._content = b'{"message": "library element could not be found"}' + resp.headers["Content-Type"] = "application/json" + return resp + + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.add_matcher(custom_matcher) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.get_library_element_by_name(LibraryElementTestCase.MissingPanelName) + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) + + @requests_mock.Mocker() + def test_get_library_element_connections(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.get( + f"http://localhost/api/library-elements/{LibraryElementTestCase.ConnectedPanelUID}/connections", + json=LibraryElementTestCase.ConnectedPanelConnectionsJSON, + ) + connections = self.grafana.libraryelement.get_library_element_connections( + LibraryElementTestCase.ConnectedPanelUID + ) + self.assertEqual(len(connections["result"]), 2) + self.assertIn(connections["result"][0]["connectionUid"], LibraryElementTestCase.ConnectedPanelConnectionUIDs) + self.assertIn(connections["result"][1]["connectionUid"], LibraryElementTestCase.ConnectedPanelConnectionUIDs) + + @requests_mock.Mocker() + def test_get_library_element_connections_grafana_pre_8_2(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.get( + f"http://localhost/api/library-elements/{LibraryElementTestCase.ConnectedPanelUID}/connections", + json=LibraryElementTestCase.ConnectedPanelConnectionsJSON, + ) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.get_library_element_connections(LibraryElementTestCase.ConnectedPanelUID) + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) + + @requests_mock.Mocker() + def test_get_library_element_connections_notfound(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.get( + f"http://localhost/api/library-elements/{LibraryElementTestCase.MissingPanelUID}/connections", + json={"message": "library element could not be found"}, + status_code=404, + ) + with self.assertRaises(GrafanaClientError) as ex: + self.grafana.libraryelement.get_library_element_connections(LibraryElementTestCase.MissingPanelUID) + self.assertEqual(404, ex.exception.status_code) + self.assertEqual("Client Error 404: library element could not be found", ex.exception.message) + + @requests_mock.Mocker() + def test_get_library_element_connections_notfound_grafana_pre_8_2(self, m): + def custom_matcher(request): + if request.path_url == "/api/health": + return None + resp = requests.Response() + resp.status_code = 404 + resp._content = b'{"message": "library element could not be found"}' + resp.headers["Content-Type"] = "application/json" + return resp + + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.add_matcher(custom_matcher) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.get_library_element(LibraryElementTestCase.MissingPanelUID) + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) + + @requests_mock.Mocker() + def test_get_library_element_connections_noconnections(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.get( + f"http://localhost/api/library-elements/{LibraryElementTestCase.UnconnectedPanelUID}/connections", + json=LibraryElementTestCase.UnconnectedPanelConnectionsJSON, + ) + connections = self.grafana.libraryelement.get_library_element_connections( + LibraryElementTestCase.UnconnectedPanelUID + ) + self.assertEqual(len(connections["result"]), 0) + + @requests_mock.Mocker() + def test_get_library_element_connections_noconnections_grafana_pre_8_2(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.get( + f"http://localhost/api/library-elements/{LibraryElementTestCase.UnconnectedPanelUID}/connections", + json=LibraryElementTestCase.UnconnectedPanelConnectionsJSON, + ) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.get_library_element_connections(LibraryElementTestCase.UnconnectedPanelUID) + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) + + @requests_mock.Mocker() + def test_create_library_element(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.post( + "http://localhost/api/library-elements", + json=LibraryElementTestCase.CreatePanelResponseJSON, + ) + resp = self.grafana.libraryelement.create_library_element( + model=LibraryElementTestCase.CreatePanelModelJSON, + name=LibraryElementTestCase.CreatePanelName, + uid=LibraryElementTestCase.CreatePanelUID, + folder_uid=LibraryElementTestCase.CreatePanelFolderUID, + ) + self.assertIsNotNone(resp["result"]) + self.assertIsNotNone(resp["result"]["folderUid"]) + self.assertEqual(resp["result"]["folderUid"], LibraryElementTestCase.CreatePanelFolderUID) + self.assertIsNotNone(resp["result"]["uid"]) + self.assertEqual(resp["result"]["uid"], LibraryElementTestCase.CreatePanelUID) + self.assertIsNotNone(resp["result"]["model"]) + + @requests_mock.Mocker() + def test_create_library_element_grafana_pre_8_2(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.post( + "http://localhost/api/library-elements", + json=LibraryElementTestCase.CreatePanelResponseJSON, + ) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.create_library_element( + model=LibraryElementTestCase.CreatePanelModelJSON, + name=LibraryElementTestCase.CreatePanelName, + uid=LibraryElementTestCase.CreatePanelUID, + folder_uid=LibraryElementTestCase.CreatePanelFolderUID, + ) + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) + + @requests_mock.Mocker() + def test_create_library_element_already_exists(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.post( + "http://localhost/api/library-elements", + json={"message": "library element with that name or UID already exists"}, + status_code=400, + ) + with self.assertRaisesRegex( + GrafanaBadInputError, "Bad Input: .*library element with that name or UID already exists.*" + ): + self.grafana.libraryelement.create_library_element( + model=LibraryElementTestCase.CreatePanelModelJSON, + name=LibraryElementTestCase.CreatePanelName, + uid=LibraryElementTestCase.CreatePanelUID, + folder_uid=LibraryElementTestCase.CreatePanelFolderUID, + ) + + @requests_mock.Mocker() + def test_update_library_element(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.patch( + f"http://localhost/api/library-elements/{LibraryElementTestCase.UpdatePanelUID}", + json=LibraryElementTestCase.UpdatePanelResponseJSON, + ) + resp = self.grafana.libraryelement.update_library_element( + model=LibraryElementTestCase.UpdatePanelModelJSON, + name=LibraryElementTestCase.UpdatePanelName, + uid=LibraryElementTestCase.UpdatePanelUID, + folder_uid=LibraryElementTestCase.UpdatePanelFolderUID, + version=1, + ) + self.assertIsNotNone(resp["result"]) + self.assertIsNotNone(resp["result"]["folderUid"]) + self.assertEqual(resp["result"]["folderUid"], LibraryElementTestCase.CreatePanelFolderUID) + self.assertIsNotNone(resp["result"]["uid"]) + self.assertEqual(resp["result"]["uid"], LibraryElementTestCase.CreatePanelUID) + self.assertIsNotNone(resp["result"]["model"]) + self.assertIsNotNone(resp["result"]["model"]["gridPos"]) + self.assertIsNotNone(resp["result"]["model"]["gridPos"]["x"]) + self.assertEqual(resp["result"]["model"]["gridPos"]["x"], 10) + self.assertIsNotNone(resp["result"]["model"]["gridPos"]["y"]) + self.assertEqual(resp["result"]["model"]["gridPos"]["y"], 15) + self.assertIsNotNone(resp["result"]["version"]) + self.assertEqual(resp["result"]["version"], 2) + + @requests_mock.Mocker() + def test_update_library_element_grafana_pre_8_2(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.patch( + f"http://localhost/api/library-elements/{LibraryElementTestCase.UpdatePanelUID}", + json=LibraryElementTestCase.UpdatePanelResponseJSON, + ) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.update_library_element( + model=LibraryElementTestCase.UpdatePanelModelJSON, + name=LibraryElementTestCase.UpdatePanelName, + uid=LibraryElementTestCase.UpdatePanelUID, + folder_uid=LibraryElementTestCase.UpdatePanelFolderUID, + version=1, + ) + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) + + @requests_mock.Mocker() + def test_update_library_element_notfound(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.patch( + f"http://localhost/api/library-elements/{LibraryElementTestCase.MissingPanelUID}", + json={"message": "library element could not be found"}, + status_code=404, + ) + with self.assertRaises(GrafanaClientError) as ex: + self.grafana.libraryelement.update_library_element( + model=LibraryElementTestCase.UpdatePanelModelJSON, + name=LibraryElementTestCase.UpdatePanelName, + uid=LibraryElementTestCase.MissingPanelUID, + folder_uid=LibraryElementTestCase.UpdatePanelFolderUID, + version=1, + ) + self.assertEqual(404, ex.exception.status_code) + self.assertEqual("Client Error 404: library element could not be found", ex.exception.message) + + @requests_mock.Mocker() + def test_delete_library_element_connections(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.delete( + f"http://localhost/api/library-elements/{LibraryElementTestCase.ConnectedPanelUID}", + json={"message": "the library element has connections"}, + status_code=403, + ) + with self.assertRaises(GrafanaClientError) as ex: + self.grafana.libraryelement.delete_library_element(LibraryElementTestCase.ConnectedPanelUID) + self.assertEqual(403, ex.exception.status_code) + self.assertEqual("Client Error 403: the library element has connections", ex.exception.message) + + @requests_mock.Mocker() + def test_delete_library_element_noconnections(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.delete( + f"http://localhost/api/library-elements/{LibraryElementTestCase.UnconnectedPanelUID}", + json={"id": 4, "message": "Library element deleted"}, + ) + resp = self.grafana.libraryelement.delete_library_element(LibraryElementTestCase.UnconnectedPanelUID) + self.assertIsNotNone(resp) + self.assertIsNotNone(resp.get("message")) + self.assertEqual("Library element deleted", resp.get("message")) + + @requests_mock.Mocker() + def test_delete_library_element_notfound(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.delete( + f"http://localhost/api/library-elements/{LibraryElementTestCase.MissingPanelUID}", + json={"message": "library element could not be found"}, + status_code=404, + ) + with self.assertRaises(GrafanaClientError) as ex: + self.grafana.libraryelement.delete_library_element(LibraryElementTestCase.MissingPanelUID) + self.assertEqual(404, ex.exception.status_code) + self.assertEqual("Client Error 404: library element could not be found", ex.exception.message) + + @requests_mock.Mocker() + def test_delete_library_element_notfound_grafana_pre_8_2(self, m): + def custom_matcher(request): + if request.path_url == "/api/health": + return None + resp = requests.Response() + resp.status_code = 404 + resp._content = b'{"message": "library element could not be found"}' + resp.headers["Content-Type"] = "application/json" + return resp + + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.add_matcher(custom_matcher) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.delete_library_element(LibraryElementTestCase.MissingPanelUID) + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) + + @requests_mock.Mocker() + def test_list_library_elements(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePost8_2, + ) + m.get( + "http://localhost/api/library-elements?", + json=LibraryElementTestCase.ListLibraryElementsResponseJSON, + ) + elements = self.grafana.libraryelement.list_library_elements() + self.assertIsNotNone(elements.get("result")) + result = elements.get("result") + self.assertIsNotNone(result.get("totalCount")) + self.assertEqual(result["totalCount"], 2) + self.assertIsNotNone(result.get("page")) + self.assertEqual(result["page"], 1) + self.assertIsNotNone(result.get("perPage")) + self.assertEqual(result["perPage"], 100) + + @requests_mock.Mocker() + def test_list_library_elements_grafana_pre_8_2(self, m): + m.get( + "http://localhost/api/health", + json=LibraryElementTestCase.HealthResponsePre8_2, + ) + m.get( + "http://localhost/api/library-elements?", + json=LibraryElementTestCase.ListLibraryElementsResponseJSON, + ) + with self.assertRaises(DeprecationWarning) as ex: + self.grafana.libraryelement.list_library_elements() + self.assertEqual( + "Grafana versions earlier than 8.2 do not support library elements", + str(ex.exception), + ) diff --git a/test/test_grafana_client.py b/test/test_grafana_client.py index 750a666..e3c6f9c 100644 --- a/test/test_grafana_client.py +++ b/test/test_grafana_client.py @@ -159,7 +159,7 @@ def test_grafana_client_version(self, mock_get): self.assertEqual(grafana.version, "9.0.1") def test_grafana_client_non_json_response(self): - grafana = GrafanaApi.from_url("https://httpbin.org/html") + grafana = GrafanaApi.from_url("https://example.org/") self.assertRaises(GrafanaClientError, lambda: grafana.connect()) def test_grafana_client_204_no_content_response(self):