diff --git a/docs/content/grafana_api/folder.md b/docs/content/grafana_api/folder.md index 6bfbcc3..c847bb4 100644 --- a/docs/content/grafana_api/folder.md +++ b/docs/content/grafana_api/folder.md @@ -7,6 +7,7 @@ * [get\_folder\_by\_id](#folder.Folder.get_folder_by_id) * [create\_folder](#folder.Folder.create_folder) * [update\_folder](#folder.Folder.update_folder) + * [move\_folder](#folder.Folder.move_folder) * [delete\_folder](#folder.Folder.delete_folder) * [get\_folder\_permissions](#folder.Folder.get_folder_permissions) * [update\_folder\_permissions](#folder.Folder.update_folder_permissions) @@ -46,6 +47,10 @@ def get_folders() -> list The method includes a functionality to extract all folders inside the organization +Required Permissions: +Action: folders:read +Scope: folders:* + **Raises**: - `Exception` - Unspecified error by executing the API call @@ -69,6 +74,10 @@ The method includes a functionality to extract all folder information specified - `uid` _str_ - Specify the uid of the folder + Required Permissions: +- `Action` - folders:read +- `Scope` - folders:* + **Raises**: @@ -94,6 +103,10 @@ The method includes a functionality to extract all folder information specified - `id` _int_ - Specify the id of the folder + Required Permissions: +- `Action` - folders:read +- `Scope` - folders:* + **Raises**: @@ -110,7 +123,7 @@ The method includes a functionality to extract all folder information specified #### create\_folder ```python -def create_folder(title: str, uid: str = None) -> dict +def create_folder(title: str, uid: str = None, parent_uid: str = None) -> dict ``` The method includes a functionality to create a new folder inside the organization specified by the defined title and the optional uid @@ -119,6 +132,11 @@ The method includes a functionality to create a new folder inside the organizati - `title` _str_ - Specify the title of the folder - `uid` _str_ - Specify the uid of the folder (default None) +- `parent_uid` _str_ - Specify the parent_uid of the folder (default None) + + Required Permissions: +- `Action` - folders:create, folders:write +- `Scope` - folders:* **Raises**: @@ -151,6 +169,10 @@ The method includes a functionality to update a folder information inside the or - `version` _int_ - Specify the version of the folder (default 0) - `overwrite` _bool_ - Should the already existing folder information be overwritten (default False) + Required Permissions: +- `Action` - folders:write +- `Scope` - folders:* + **Raises**: @@ -162,6 +184,36 @@ The method includes a functionality to update a folder information inside the or - `api_call` _dict_ - Returns an updated folder + + +#### move\_folder + +```python +def move_folder(uid: str, parent_uid: str = None) +``` + +The method includes a functionality to move a folder inside the organization specified by the defined uid. This feature is only relevant if nested folders are enabled + +**Arguments**: + +- `uid` _str_ - Specify the uid of the folder +- `parent_uid` _str_ - Specify the parent_uid of the folder. If the value is None, then the folder is moved under the root (default None) + + Required Permissions: +- `Action` - folders:create, folders:write +- `Scope` - folders:*, folders:uid: + + +**Raises**: + +- `ValueError` - Missed specifying a necessary value +- `Exception` - Unspecified error by executing the API call + + +**Returns**: + +- `api_call` _dict_ - Returns the moved folder + #### delete\_folder @@ -176,6 +228,10 @@ The method includes a functionality to delete a folder inside the organization s - `uid` _str_ - Specify the uid of the folder + Required Permissions: +- `Action` - folders:delete +- `Scope` - folders:* + **Raises**: @@ -201,6 +257,10 @@ The method includes a functionality to extract the folder permissions inside the - `uid` _str_ - Specify the uid of the folder + Required Permissions: +- `Action` - folders.permissions:read +- `Scope` - folders:* + **Raises**: @@ -227,6 +287,10 @@ The method includes a functionality to update the folder permissions based on th - `uid` _str_ - Specify the uid of the folder - `permission_json` _dict_ - Specify the inserted permissions as dict + Required Permissions: +- `Action` - folders.permissions:write +- `Scope` - folders:* + **Raises**: diff --git a/docs/coverage.svg b/docs/coverage.svg index 6bfc8fa..e5db27c 100644 --- a/docs/coverage.svg +++ b/docs/coverage.svg @@ -15,7 +15,7 @@ coverage coverage - 99% - 99% + 100% + 100% diff --git a/grafana_api/alerting.py b/grafana_api/alerting.py index 221e8f2..f4770eb 100644 --- a/grafana_api/alerting.py +++ b/grafana_api/alerting.py @@ -151,11 +151,7 @@ def delete_alertmanager_silence_by_id( RequestsMethods.DELETE, ) - if ( - api_call == dict() - or api_call.get("message") - != "silence deleted" - ): + if api_call == dict() or api_call.get("message") != "silence deleted": logging.error(f"Please, check the error: {api_call}.") raise Exception else: @@ -258,7 +254,9 @@ def create_or_update_alertmanager_silence( json.dumps(silence_json_dict), ) - if api_call == dict() or (api_call.get("id") is None and api_call.get("silenceID") is None): + if api_call == dict() or ( + api_call.get("id") is None and api_call.get("silenceID") is None + ): logging.error(f"Check the error: {api_call}.") raise Exception else: @@ -661,7 +659,12 @@ def create_or_update_ruler_group_by_namespace( None """ - if len(datasource_uid) != 0 and len(namespace) != 0 and len(group_name) != 0 and rules != list(): + if ( + len(datasource_uid) != 0 + and len(namespace) != 0 + and len(group_name) != 0 + and rules != list() + ): rules_json_list: list = list() for rule in rules: @@ -696,7 +699,9 @@ def create_or_update_ruler_group_by_namespace( else: logging.info("You successfully created an ruler group.") else: - logging.error("There is no datasource_uid, namespace, name or rules defined.") + logging.error( + "There is no datasource_uid, namespace, name or rules defined." + ) raise ValueError def delete_ruler_group( @@ -729,7 +734,9 @@ def delete_ruler_group( else: logging.info("You successfully deleted a ruler group.") else: - logging.error("There is no datasource_uid, namespace or group_name defined.") + logging.error( + "There is no datasource_uid, namespace or group_name defined." + ) raise ValueError def get_ruler_group( @@ -761,7 +768,9 @@ def get_ruler_group( else: return api_call else: - logging.error("There is no datasource_uid, namespace or group_name defined.") + logging.error( + "There is no datasource_uid, namespace or group_name defined." + ) raise ValueError def test_rule(self, data_query: list) -> dict: @@ -823,7 +832,11 @@ def test_rule(self, data_query: list) -> dict: raise ValueError def test_datasource_uid_rule( - self, expr: str, condition: str, data_query: list, datasource_uid: str = "grafana" + self, + expr: str, + condition: str, + data_query: list, + datasource_uid: str = "grafana", ) -> dict: """The method includes a functionality to test a datasource uid rule specified by the expr, the condition, a list of data queries and the datasource_uid @@ -841,7 +854,12 @@ def test_datasource_uid_rule( api_call (dict): Returns the result of the specified datasource_uid rule """ - if len(datasource_uid) != 0 and len(expr) != 0 and len(condition) != 0 and data_query != list(): + if ( + len(datasource_uid) != 0 + and len(expr) != 0 + and len(condition) != 0 + and data_query != list() + ): datasource_rule_query_objects_json: list = list() datasource_rule_query_object_json: dict = dict() @@ -1045,9 +1063,7 @@ def create_or_update_ngalert_organization_configuration( "You successfully created an NGAlert organization configuration." ) else: - logging.error( - "There is no alert_managers or alertmanagers_choice defined." - ) + logging.error("There is no alert_managers or alertmanagers_choice defined.") raise ValueError def get_ngalert_alertmanagers_by_organization(self) -> dict: diff --git a/grafana_api/folder.py b/grafana_api/folder.py index 7a695c5..cc5826b 100644 --- a/grafana_api/folder.py +++ b/grafana_api/folder.py @@ -23,6 +23,10 @@ def __init__(self, grafana_api_model: APIModel): def get_folders(self) -> list: """The method includes a functionality to extract all folders inside the organization + Required Permissions: + Action: folders:read + Scope: folders:* + Raises: Exception: Unspecified error by executing the API call @@ -46,6 +50,10 @@ def get_folder_by_uid(self, uid: str) -> dict: Args: uid (str): Specify the uid of the folder + Required Permissions: + Action: folders:read + Scope: folders:* + Raises: ValueError: Missed specifying a necessary value Exception: Unspecified error by executing the API call @@ -74,6 +82,10 @@ def get_folder_by_id(self, id: int) -> dict: Args: id (int): Specify the id of the folder + Required Permissions: + Action: folders:read + Scope: folders:* + Raises: ValueError: Missed specifying a necessary value Exception: Unspecified error by executing the API call @@ -96,12 +108,19 @@ def get_folder_by_id(self, id: int) -> dict: logging.error("There is no folder id defined.") raise ValueError - def create_folder(self, title: str, uid: str = None) -> dict: + def create_folder( + self, title: str, uid: str = None, parent_uid: str = None + ) -> dict: """The method includes a functionality to create a new folder inside the organization specified by the defined title and the optional uid Args: title (str): Specify the title of the folder uid (str): Specify the uid of the folder (default None) + parent_uid (str): Specify the parent_uid of the folder (default None) + + Required Permissions: + Action: folders:create, folders:write + Scope: folders:* Raises: ValueError: Missed specifying a necessary value @@ -118,6 +137,9 @@ def create_folder(self, title: str, uid: str = None) -> dict: if uid is not None and len(uid) != 0: folder_information.update({"uid": uid}) + if parent_uid is not None and len(parent_uid) != 0: + folder_information.update({"parentUid": parent_uid}) + api_call: dict = Api(self.grafana_api_model).call_the_api( APIEndpoints.FOLDERS.value, RequestsMethods.POST, @@ -144,6 +166,10 @@ def update_folder( version (int): Specify the version of the folder (default 0) overwrite (bool): Should the already existing folder information be overwritten (default False) + Required Permissions: + Action: folders:write + Scope: folders:* + Raises: ValueError: Missed specifying a necessary value Exception: Unspecified error by executing the API call @@ -179,12 +205,56 @@ def update_folder( logging.error("There is no folder title, version or uid defined.") raise ValueError + def move_folder(self, uid: str, parent_uid: str = None): + """The method includes a functionality to move a folder inside the organization specified by the defined uid. This feature is only relevant if nested folders are enabled + + Args: + uid (str): Specify the uid of the folder + parent_uid (str): Specify the parent_uid of the folder. If the value is None, then the folder is moved under the root (default None) + + Required Permissions: + Action: folders:create, folders:write + Scope: folders:*, folders:uid: + + Raises: + ValueError: Missed specifying a necessary value + Exception: Unspecified error by executing the API call + + Returns: + api_call (dict): Returns the moved folder + """ + + if len(uid) != 0: + folder_information: dict = dict() + + if parent_uid is not None and len(parent_uid) != 0: + folder_information.update({"parentUid": parent_uid}) + + api_call = Api(self.grafana_api_model).call_the_api( + f"{APIEndpoints.FOLDERS.value}/{uid}/move", + RequestsMethods.POST, + json.dumps(folder_information), + ) + + if api_call == dict() or api_call.get("id") is None: + logging.error(f"Please, check the error: {api_call}.") + raise Exception + else: + return api_call + else: + logging.error("There is no folder uid defined.") + raise ValueError + def delete_folder(self, uid: str): """The method includes a functionality to delete a folder inside the organization specified by the defined uid Args: uid (str): Specify the uid of the folder + Required Permissions: + Action: folders:delete + Scope: folders:* + Raises: ValueError: Missed specifying a necessary value Exception: Unspecified error by executing the API call @@ -220,6 +290,10 @@ def get_folder_permissions(self, uid: str) -> list: Args: uid (str): Specify the uid of the folder + Required Permissions: + Action: folders.permissions:read + Scope: folders:* + Raises: ValueError: Missed specifying a necessary value Exception: Unspecified error by executing the API call @@ -250,6 +324,10 @@ def update_folder_permissions(self, uid: str, permission_json: dict): uid (str): Specify the uid of the folder permission_json (dict): Specify the inserted permissions as dict + Required Permissions: + Action: folders.permissions:write + Scope: folders:* + Raises: ValueError: Missed specifying a necessary value Exception: Unspecified error by executing the API call diff --git a/setup.py b/setup.py index 4afbf5a..e2800af 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setuptools.setup( name="grafana-api-sdk", - version="0.6.0", + version="0.6.1", author="Pascal Zimmermann", author_email="info@theiotstudio.com", description="A Grafana API SDK", diff --git a/tests/integrationtest/test_alerting.py b/tests/integrationtest/test_alerting.py index 166cb37..e8e3657 100644 --- a/tests/integrationtest/test_alerting.py +++ b/tests/integrationtest/test_alerting.py @@ -6,7 +6,6 @@ from grafana_api.model import ( APIModel, AlertmanagerConfig, - AlertmanagerReceivers, DatasourceRuleQuery, ) from grafana_api.alerting import Alerting @@ -66,16 +65,27 @@ def test_c_delete_alertmanager_config(self): "group_by": ["grafana_folder", "alertname"], "receiver": "grafana-default-email", "provenance": "api", - "routes": [{"object_matchers": [["__grafana_autogenerated__", - "=", - "true"]], - "receiver": "grafana-default-email", - "routes": [{"group_by": ["grafana_folder", - "alertname"], - "object_matchers": [["__grafana_receiver__", - "=", - "grafana-default-email"]], - "receiver": "grafana-default-email"}]}] + "routes": [ + { + "object_matchers": [ + ["__grafana_autogenerated__", "=", "true"] + ], + "receiver": "grafana-default-email", + "routes": [ + { + "group_by": ["grafana_folder", "alertname"], + "object_matchers": [ + [ + "__grafana_receiver__", + "=", + "grafana-default-email", + ] + ], + "receiver": "grafana-default-email", + } + ], + } + ], }, "receivers": [ { @@ -92,7 +102,7 @@ def test_c_delete_alertmanager_config(self): ], } ], - } + }, } self.assertEqual(result, self.alerting.get_alertmanager_config()) @@ -137,8 +147,8 @@ def test_get_prometheus_alerts(self): for i in range(0, MAX_TRIES): if ( - len(self.alerting.get_prometheus_alerts().get("data").get("alerts")) - != 0 + len(self.alerting.get_prometheus_alerts().get("data").get("alerts")) + != 0 ): time.sleep(0.1 + i / 2) self.assertEqual( diff --git a/tests/integrationtest/test_folder.py b/tests/integrationtest/test_folder.py index 0dc700f..8a60a43 100644 --- a/tests/integrationtest/test_folder.py +++ b/tests/integrationtest/test_folder.py @@ -69,14 +69,38 @@ def test_a_create_folder(self): self.assertEqual("test1", self.folder.get_folders()[1].get("title")) - def test_b_update_folder(self): + def test_b_subfolder(self): + parent_uid = self.folder.get_folders()[1].get("uid") + + subfolder: dict = self.folder.create_folder("test2", parent_uid=parent_uid) + + self.assertEqual( + "test2", self.folder.get_folder_by_uid(subfolder["uid"]).get("title") + ) + + def test_c_update_folder(self): self.folder.update_folder( "test2", self.folder.get_folders()[1].get("uid"), version=1 ) self.assertEqual("test2", self.folder.get_folders()[1].get("title")) - def test_c_delete_folder(self): + def test_d_move_folder(self): + parent_uid = self.folder.get_folders()[1].get("uid") + + folder_uid_a = self.folder.create_folder("test11", parent_uid=parent_uid)["uid"] + folder_uid_b = self.folder.create_folder("test12")["uid"] + + self.assertEqual("test12", self.folder.get_folder_by_uid(folder_uid_b)["title"]) + + moved_folder: dict = self.folder.move_folder( + folder_uid_a, parent_uid=folder_uid_b + ) + + self.assertEqual("test12", moved_folder["parents"][0]["title"]) + self.folder.delete_folder(moved_folder["parents"][0]["uid"]) + + def test_e_delete_folder(self): self.folder.delete_folder(self.folder.get_folders()[1].get("uid")) self.assertEqual(1, len(self.folder.get_folders())) @@ -110,7 +134,7 @@ def test_get_folder_permissions(self): self.folder.get_folder_permissions("6U_QdWJnz"), ) - def test_d_update_folder_permissions(self): + def test_f_update_folder_permissions(self): permission_dict: dict = dict({"items": [{"role": "Viewer", "permission": 2}]}) self.folder.update_folder_permissions("6U_QdWJnz", permission_dict) diff --git a/tests/integrationtest/test_playlist.py b/tests/integrationtest/test_playlist.py index e1a781e..c6f01c5 100644 --- a/tests/integrationtest/test_playlist.py +++ b/tests/integrationtest/test_playlist.py @@ -18,11 +18,14 @@ def test_search_playlist(self): self.assertEqual("Test1", self.playlist.search_playlist()[0].get("name")) def test_get_playlist(self): - self.assertEqual("Test1", self.playlist.get_playlist("edq1prp6dfy80c").get("name")) + self.assertEqual( + "Test1", self.playlist.get_playlist("edq1prp6dfy80c").get("name") + ) def test_get_playlist_items(self): self.assertEqual( - "dashboard_by_uid", self.playlist.get_playlist_items("edq1prp6dfy80c")[0].get("type") + "dashboard_by_uid", + self.playlist.get_playlist_items("edq1prp6dfy80c")[0].get("type"), ) def test_a_create_playlist(self): diff --git a/tests/unittests/test_alerting.py b/tests/unittests/test_alerting.py index 80890b3..88c8cb7 100644 --- a/tests/unittests/test_alerting.py +++ b/tests/unittests/test_alerting.py @@ -203,7 +203,9 @@ def test_create_or_update_alertmanager_silence(self, call_the_api_mock): ) @patch("grafana_api.api.Api.call_the_api") - def test_create_or_update_alertmanager_silence_return_silence_id(self, call_the_api_mock): + def test_create_or_update_alertmanager_silence_return_silence_id( + self, call_the_api_mock + ): model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) alerting: Alerting = Alerting(grafana_api_model=model) silence: Silence = Silence( @@ -738,7 +740,7 @@ def test_test_backtest_rule(self, call_the_api_mock): model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) alerting: Alerting = Alerting(grafana_api_model=model) datasource_rule_query: DatasourceRuleQuery = DatasourceRuleQuery( - "test", {"test": "test"}, "test", "test", {"test": "test"} + "test", {"test": "test"}, "datasourceUid", "test", {"test": "test"} ) call_the_api_mock.return_value = dict({"test": "test"}) @@ -756,17 +758,30 @@ def test_test_backtest_rule_no_condition(self): alerting.test_backtest_rule("", list()) @patch("grafana_api.api.Api.call_the_api") - def test_test_recipient_rule_test_not_possible(self, call_the_api_mock): + def test_test_backtest_rule_no_fields(self, call_the_api_mock): model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) alerting: Alerting = Alerting(grafana_api_model=model) datasource_rule_query: DatasourceRuleQuery = DatasourceRuleQuery( - "test", {"test": "test"}, "test", "test", {"test": "test"} + "test", {"test": "test"}, "datasourceUid", "test", {"test": "test"} + ) + + call_the_api_mock.return_value = dict() + + with self.assertRaises(Exception): + alerting.test_backtest_rule("test", [datasource_rule_query]) + + @patch("grafana_api.api.Api.call_the_api") + def test_test_datasource_uid_rule_test_not_possible(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + alerting: Alerting = Alerting(grafana_api_model=model) + datasource_rule_query: DatasourceRuleQuery = DatasourceRuleQuery( + "test", {"test": "test"}, "datasourceUid", "test", {"test": "test"} ) call_the_api_mock.return_value = dict() with self.assertRaises(Exception): - alerting.test_recipient_rule("test", "test", [datasource_rule_query]) + alerting.test_datasource_uid_rule("test", "test", [datasource_rule_query]) @patch("grafana_api.api.Api.call_the_api") def test_delete_ngalert_organization_configuration(self, call_the_api_mock): diff --git a/tests/unittests/test_api.py b/tests/unittests/test_api.py index 00a8ec6..c62b384 100644 --- a/tests/unittests/test_api.py +++ b/tests/unittests/test_api.py @@ -38,12 +38,16 @@ def test_call_the_api_basic_auth(self, httpx_client_mock): @patch("httpx.Client") def test_call_the_api_with_proxy_headers(self, httpx_client_mock): model: APIModel = APIModel( - host="https://test.test.de", username="test", password="test", - headers=dict({"X-Custom-Header": "custom_value"}) + host="https://test.test.de", + username="test", + password="test", + headers=dict({"X-Custom-Header": "custom_value"}), ) api: Api = Api(grafana_api_model=model) - httpx_client_mock.return_value.request.return_value.text = '{"status": "success"}' + httpx_client_mock.return_value.request.return_value.text = ( + '{"status": "success"}' + ) self.assertEqual( "success", @@ -51,7 +55,17 @@ def test_call_the_api_with_proxy_headers(self, httpx_client_mock): ) httpx_client_mock.assert_called() - self.assertEqual(dict({"X-Custom-Header": "custom_value", "Authorization": "Basic dGVzdDp0ZXN0", "Content-Type": "application/json", "Accept": "application/json"}), httpx_client_mock.call_args[1]["headers"]) + self.assertEqual( + dict( + { + "X-Custom-Header": "custom_value", + "Authorization": "Basic dGVzdDp0ZXN0", + "Content-Type": "application/json", + "Accept": "application/json", + } + ), + httpx_client_mock.call_args[1]["headers"], + ) @patch("httpx.Client") def test_call_the_api_org_id(self, httpx_client_mock): diff --git a/tests/unittests/test_folder.py b/tests/unittests/test_folder.py index 9d36a36..2171723 100644 --- a/tests/unittests/test_folder.py +++ b/tests/unittests/test_folder.py @@ -108,6 +108,20 @@ def test_create_folder_specified_uid(self, call_the_api_mock): folder.create_folder("test", "test"), ) + @patch("grafana_api.api.Api.call_the_api") + def test_create_folder_parent_uid(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + folder: Folder = Folder(grafana_api_model=model) + + call_the_api_mock.return_value = dict( + {"title": None, "id": 12, "parent_uid": "test"} + ) + + self.assertEqual( + dict({"title": None, "id": 12, "parent_uid": "test"}), + folder.create_folder("test", parent_uid="test"), + ) + @patch("grafana_api.api.Api.call_the_api") def test_create_folder_no_title(self, call_the_api_mock): model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) @@ -184,6 +198,47 @@ def test_update_folder_error_response(self, call_the_api_mock): with self.assertRaises(Exception): folder.update_folder("test", "test", 10) + @patch("grafana_api.api.Api.call_the_api") + def test_move_folder(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + folder: Folder = Folder(grafana_api_model=model) + + call_the_api_mock.return_value = dict({"title": "test1", "id": 12}) + + self.assertEqual( + dict({"title": "test1", "id": 12}), + folder.move_folder("test"), + ) + + def test_move_folder_no_uid(self): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + folder: Folder = Folder(grafana_api_model=model) + + with self.assertRaises(ValueError): + folder.move_folder("") + + @patch("grafana_api.api.Api.call_the_api") + def test_move_folder_parent_uid(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + folder: Folder = Folder(grafana_api_model=model) + + call_the_api_mock.return_value = dict({"title": "test", "id": 12}) + + self.assertEqual( + dict({"title": "test", "id": 12}), + folder.move_folder("test", "test"), + ) + + @patch("grafana_api.api.Api.call_the_api") + def test_move_folder_no_id(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + folder: Folder = Folder(grafana_api_model=model) + + call_the_api_mock.return_value = dict({}) + + with self.assertRaises(Exception): + folder.move_folder("test") + @patch("grafana_api.api.Api.call_the_api") def test_delete_folder(self, call_the_api_mock): model: APIModel = APIModel(host=MagicMock(), token=MagicMock())