diff --git a/splunklib/client.py b/splunklib/client.py index 038980ea4..8e52ae251 100644 --- a/splunklib/client.py +++ b/splunklib/client.py @@ -64,6 +64,7 @@ import socket from datetime import datetime, timedelta from time import sleep +from typing import Any from urllib import parse try: @@ -102,6 +103,7 @@ def deprecated(message): # pyright: ignore[reportUnknownParameterType] PATH_APPS = "apps/local/" PATH_CAPABILITIES = "authorization/capabilities/" PATH_CONF = "configs/conf-%s/" +PATH_DASHBOARDS = "data/ui/views/" PATH_PROPERTIES = "properties/" PATH_DEPLOYMENT_CLIENTS = "deployment/client/" PATH_DEPLOYMENT_TENANTS = "deployment/tenants/" @@ -481,6 +483,15 @@ def capabilities(self): response = self.get(PATH_CAPABILITIES) return _load_atom(response, MATCH_ENTRY_CONTENT).capabilities + @property + def dashboards(self): + """Returns the collection of dashboards for this Splunk instance. + + :return: A :class:`Dashboards` collection of :class:`Dashboard` + entities. + """ + return Dashboards(self) + @property def event_types(self): """Returns the collection of event types defined in this Splunk instance. @@ -3653,6 +3664,42 @@ def create(self, name, definition, **kwargs): return Collection.create(self, name, definition=definition, **kwargs) +class Dashboard(Entity): + """This class represents a dashboard (view) in Splunk.""" + + def __init__(self, service: "Service", path: str, **kwargs: Any) -> None: + Entity.__init__(self, service, path, **kwargs) + + def export(self) -> str: + """Returns the dashboard XML content. + + :return: The dashboard XML definition. + :rtype: ``string`` + """ + return self.content.get("eai:data", "") # pyright: ignore[reportUnknownVariableType] + + +class Dashboards(Collection): + """This class represents a collection of dashboards. Retrieve this + collection using :meth:`Service.dashboards`.""" + + def __init__(self, service: "Service") -> None: + Collection.__init__(self, service, PATH_DASHBOARDS, item=Dashboard) + + def create(self, name: str, xml: str, **kwargs: Any) -> Entity: # pyright: ignore[reportIncompatibleMethodOverride,reportImplicitOverride] + """Creates a dashboard. + + :param name: The name for the dashboard. + :type name: ``string`` + :param xml: The dashboard XML definition. + :type xml: ``string`` + :param kwargs: Additional arguments (optional). + :type kwargs: ``dict`` + :return: The :class:`Dashboard` entity. + """ + return Collection.create(self, name, **{"eai:data": xml, **kwargs}) # pyright: ignore[reportUnknownVariableType] + + class Settings(Entity): """This class represents configuration settings for a Splunk service. Retrieve this collection using :meth:`Service.settings`.""" diff --git a/tests/unit/test_dashboard.py b/tests/unit/test_dashboard.py new file mode 100644 index 000000000..441a8b4ae --- /dev/null +++ b/tests/unit/test_dashboard.py @@ -0,0 +1,60 @@ +# Copyright © 2011-2026 Splunk, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest.mock import MagicMock, patch + +import pytest + +from splunklib.client import ( + PATH_DASHBOARDS, + Collection, + Dashboard, + Dashboards, + Entity, +) + + +class TestDashboard: + def test_is_entity_subclass(self) -> None: + assert issubclass(Dashboard, Entity) + + def test_path_constant(self) -> None: + assert PATH_DASHBOARDS == "data/ui/views/" + + def test_export_returns_eai_data(self) -> None: + dashboard = MagicMock(spec=Dashboard) + dashboard.content = {"eai:data": ""} + assert Dashboard.export(dashboard) == "" + + def test_export_returns_empty_when_missing(self) -> None: + dashboard = MagicMock(spec=Dashboard) + dashboard.content = {} + assert Dashboard.export(dashboard) == "" + + +class TestDashboards: + def test_is_collection_subclass(self) -> None: + assert issubclass(Dashboards, Collection) + + @patch.object(Collection, "create") + def test_create_passes_xml_as_eai_data(self, mock_create: MagicMock) -> None: + dashboards = Dashboards.__new__(Dashboards) + xml = "" + Dashboards.create(dashboards, "test_dash", xml) + mock_create.assert_called_once_with(dashboards, "test_dash", **{"eai:data": xml}) + + def test_create_raises_on_missing_xml(self) -> None: + dashboards = Dashboards.__new__(Dashboards) + with pytest.raises(TypeError): + Dashboards.create(dashboards, "test_dash") # pyright: ignore[reportCallIssue]