From 7bd2a457b86a1b7d3003c92cb9005eef382a51a8 Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Sun, 22 May 2022 12:14:52 +0200 Subject: [PATCH 1/5] Init the repo --- src/grafana_api/model.py | 29 +++++++++ src/grafana_api/query_history.py | 84 ++++++++++++++++++++++++++ tests/integrationtest/query_history.py | 0 tests/unittests/query_history.py | 0 4 files changed, 113 insertions(+) create mode 100644 src/grafana_api/query_history.py create mode 100644 tests/integrationtest/query_history.py create mode 100644 tests/unittests/query_history.py diff --git a/src/grafana_api/model.py b/src/grafana_api/model.py index 4ff72f4..b6e0ae5 100644 --- a/src/grafana_api/model.py +++ b/src/grafana_api/model.py @@ -31,6 +31,7 @@ class APIEndpoints(Enum): DASHBOARD_SNAPSHOTS = f"{api_prefix}/dashboard/snapshots" PLAYLISTS = f"{api_prefix}/playlists" TEAMS = f"{api_prefix}/teams" + QUERY_HISTORY = f"{api_prefix}/query-history" class RequestsMethods(Enum): @@ -264,3 +265,31 @@ class TeamObject(NamedTuple): name: str email: str org_id: int + + +class QueryDatasourceObject(NamedTuple): + """The class includes all necessary variables to generate a query datasource object that is necessary to create a query history object + + Args: + type (str): Specify the type of the datasource query + uid (str): Specify the uid of the datasource query + """ + + type: str + uid: str + + +class QueryObject(NamedTuple): + """The class includes all necessary variables to generate a query object that is necessary to create a query history + + Args: + ref_id (str): Specify the ref_id of the query history + key (str): Specify the key of the query history + scenario_id (str): Specify the scenario_id of the query history + datasource (QueryDatasourceObject): Specify the datasource of the type QueryDatasourceObject + """ + + ref_id: str + key: str + scenario_id: str + datasource: QueryDatasourceObject diff --git a/src/grafana_api/query_history.py b/src/grafana_api/query_history.py new file mode 100644 index 0000000..f2c5e50 --- /dev/null +++ b/src/grafana_api/query_history.py @@ -0,0 +1,84 @@ +import json +import logging + +from .model import ( + APIModel, + APIEndpoints, + RequestsMethods, + TeamObject, +) +from .api import Api + + +class QueryHistory: + """The class includes all necessary methods to access the Grafana query history API endpoints. Be aware that it requires that the user is logged in and that Query history feature is enabled in config file + + Args: + grafana_api_model (APIModel): Inject a Grafana API model object that includes all necessary values and information + + Attributes: + grafana_api_model (APIModel): This is where we store the grafana_api_model + """ + + def __init__(self, grafana_api_model: APIModel): + self.grafana_api_model = grafana_api_model + + def add_query_to_history(self, datasource_uid: str, queries: list) -> dict: + """The method includes a functionality to get the organization teams specified by the optional pagination functionality + + Args: + datasource_uid (str): Specify the datasource uid + queries (list): Specify the queries as list from type QueryObject + + Raises: + Exception: Unspecified error by executing the API call + + Returns: + api_call (dict): Returns the organization teams + """ + + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api( + APIEndpoints.QUERY_HISTORY.value, + RequestsMethods.POST, + json.dumps(dict({"datasourceUid": datasource_uid, "queries": queries})) + ) + .json() + ) + + if api_call == dict() or api_call.get("result") is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call + + def search_history_query(self, datasource_uid: str, queries: list) -> dict: + """The method includes a functionality to get the organization teams specified by the optional pagination functionality + + Args: + datasource_uid (str): Specify the datasource uid + queries (list): Specify the queries as list from type QueryObject + + Raises: + Exception: Unspecified error by executing the API call + + Returns: + api_call (dict): Returns the organization teams + """ + + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api( + APIEndpoints.QUERY_HISTORY.value, + RequestsMethods.POST, + json.dumps(dict({"datasourceUid": datasource_uid, "queries": queries})) + ) + .json() + ) + + if api_call == dict() or api_call.get("result") is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call \ No newline at end of file diff --git a/tests/integrationtest/query_history.py b/tests/integrationtest/query_history.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unittests/query_history.py b/tests/unittests/query_history.py new file mode 100644 index 0000000..e69de29 From 2b88d6aed43ab499ffe9d939c98f9eee97a224ef Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Fri, 27 May 2022 10:29:31 +0200 Subject: [PATCH 2/5] WIP --- src/grafana_api/api.py | 12 +- src/grafana_api/licensing.py | 111 ++++++++ src/grafana_api/model.py | 4 + src/grafana_api/organisation.py | 36 +-- src/grafana_api/other_http.py | 100 ++++++++ src/grafana_api/query_history.py | 242 +++++++++++++++--- src/grafana_api/reporting.py | 71 +++++ .../{query_history.py => test_other_http.py} | 0 tests/integrationtest/test_query_history.py | 33 +++ tests/unittests/test_licensing.py | 74 ++++++ .../{query_history.py => test_other_http.py} | 0 tests/unittests/test_query_history.py | 241 +++++++++++++++++ tests/unittests/test_reporting.py | 45 ++++ 13 files changed, 913 insertions(+), 56 deletions(-) create mode 100644 src/grafana_api/licensing.py create mode 100644 src/grafana_api/other_http.py create mode 100644 src/grafana_api/reporting.py rename tests/integrationtest/{query_history.py => test_other_http.py} (100%) create mode 100644 tests/integrationtest/test_query_history.py create mode 100644 tests/unittests/test_licensing.py rename tests/unittests/{query_history.py => test_other_http.py} (100%) create mode 100644 tests/unittests/test_query_history.py create mode 100644 tests/unittests/test_reporting.py diff --git a/src/grafana_api/api.py b/src/grafana_api/api.py index 179e9bd..dfb213d 100644 --- a/src/grafana_api/api.py +++ b/src/grafana_api/api.py @@ -23,6 +23,7 @@ def call_the_api( api_call: str, method: RequestsMethods = RequestsMethods.GET, json_complete: str = None, + timeout: float = None, ) -> any: """The method execute a defined API call against the Grafana endpoints @@ -30,6 +31,7 @@ def call_the_api( api_call (str): Specify the API call endpoint method (RequestsMethods): Specify the used method (default GET) json_complete (str): Specify the inserted JSON as string + timeout (float): Specify the timeout for the corresponding API call Raises: Exception: Unspecified error by executing the API call @@ -58,12 +60,12 @@ def call_the_api( try: if method.value == RequestsMethods.GET.value: return Api.__check_the_api_call_response( - requests.get(api_url, headers=headers) + requests.get(api_url, headers=headers, timeout=timeout) ) elif method.value == RequestsMethods.PUT.value: if json_complete is not None: return Api.__check_the_api_call_response( - requests.put(api_url, data=json_complete, headers=headers) + requests.put(api_url, data=json_complete, headers=headers, timeout=timeout) ) else: logging.error("Please define the json_complete.") @@ -71,7 +73,7 @@ def call_the_api( elif method.value == RequestsMethods.POST.value: if json_complete is not None: return Api.__check_the_api_call_response( - requests.post(api_url, data=json_complete, headers=headers) + requests.post(api_url, data=json_complete, headers=headers, timeout=timeout) ) else: logging.error("Please define the json_complete.") @@ -79,14 +81,14 @@ def call_the_api( elif method.value == RequestsMethods.PATCH.value: if json_complete is not None: return Api.__check_the_api_call_response( - requests.patch(api_url, data=json_complete, headers=headers) + requests.patch(api_url, data=json_complete, headers=headers, timeout=timeout) ) else: logging.error("Please define the json_complete.") raise Exception elif method.value == RequestsMethods.DELETE.value: return Api.__check_the_api_call_response( - requests.delete(api_url, headers=headers) + requests.delete(api_url, headers=headers, timeout=timeout) ) else: logging.error("Please define a valid method.") diff --git a/src/grafana_api/licensing.py b/src/grafana_api/licensing.py new file mode 100644 index 0000000..0c15eff --- /dev/null +++ b/src/grafana_api/licensing.py @@ -0,0 +1,111 @@ +import json +from requests import Response +import logging + +from .model import ( + APIModel, + APIEndpoints, + RequestsMethods, +) +from .api import Api + + +class Licensing: + """The class includes all necessary methods to access the Grafana licensing API endpoints. Be aware that the functionality is a Grafana ENTERPRISE v7.4+ feature + + HINT: Note Grafana Enterprise API need required permissions if fine-grained access control is enabled + + Args: + grafana_api_model (APIModel): Inject a Grafana API model object that includes all necessary values and information + + Attributes: + grafana_api_model (APIModel): This is where we store the grafana_api_model + """ + + def __init__(self, grafana_api_model: APIModel): + self.grafana_api_model = grafana_api_model + + def check_license_availability(self): + """The method includes a functionality to checks if a valid license is available + + Required Permissions: + Action: licensing:read + Scope: N/A + + Raises: + Exception: Unspecified error by executing the API call + + Returns: + api_call (bool): Returns the result if the license is available or not + """ + + api_call: Response = ( + Api(self.grafana_api_model) + .call_the_api( + f"{APIEndpoints.LICENSING.value}/check", + ) + ) + + if api_call.status_code != 200: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return json.loads(str(api_call.text)) + + def manually_force_license_refresh(self): + """The method includes a functionality to manually ask license issuer for a new token + + Required Permissions: + Action: licensing:update + Scope: N/A + + Raises: + Exception: Unspecified error by executing the API call + + Returns: + api_call (dict): Returns the result of license refresh call + """ + + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api( + f"{APIEndpoints.LICENSING.value}/token/renew", + RequestsMethods.POST, + json.dumps({}), + ) + .json() + ) + + if api_call == dict() or api_call.get("jti") is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call + + def remove_license_from_dashboard(self): + """The method includes a functionality to removes the license stored in the Grafana database + + Required Permissions: + Action: licensing:delete + Scope: N/A + + Raises: + Exception: Unspecified error by executing the API call + + Returns: + api_call (dict): Returns the result of license refresh call + """ + + api_call: Response = ( + Api(self.grafana_api_model) + .call_the_api( + f"{APIEndpoints.LICENSING.value}/token", + RequestsMethods.DELETE, + ) + ) + + if api_call.status_code != 200: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + logging.info("You successfully removed the corresponding license from the database.") diff --git a/src/grafana_api/model.py b/src/grafana_api/model.py index b6e0ae5..f3f3982 100644 --- a/src/grafana_api/model.py +++ b/src/grafana_api/model.py @@ -32,6 +32,10 @@ class APIEndpoints(Enum): PLAYLISTS = f"{api_prefix}/playlists" TEAMS = f"{api_prefix}/teams" QUERY_HISTORY = f"{api_prefix}/query-history" + REPORTING = f"{api_prefix}/reports/email" + LICENSING = f"{api_prefix}/licensing" + FRONTEND = f"{api_prefix}/frontend" + LOGIN = f"{api_prefix}/login" class RequestsMethods(Enum): diff --git a/src/grafana_api/organisation.py b/src/grafana_api/organisation.py index f71d12a..562761f 100644 --- a/src/grafana_api/organisation.py +++ b/src/grafana_api/organisation.py @@ -19,7 +19,7 @@ def __init__(self, grafana_api_model: APIModel): self.grafana_api_model = grafana_api_model def get_current_organization(self) -> dict: - """The method includes a functionality to get the current organization. + """The method includes a functionality to get the current organization Required Permissions: Action: orgs:read @@ -45,7 +45,7 @@ def get_current_organization(self) -> dict: return api_call def get_all_users_by_the_current_organization(self) -> list: - """The method includes a functionality to get all users from the current organization. + """The method includes a functionality to get all users from the current organization Required Permissions: Action: org.users:read @@ -71,7 +71,7 @@ def get_all_users_by_the_current_organization(self) -> list: return api_call def get_all_users_by_the_current_organization_lookup(self) -> list: - """The method includes a functionality to get the lookup information of all users from the current organization. + """The method includes a functionality to get the lookup information of all users from the current organization Required Permissions: Action: org.users:read @@ -97,7 +97,7 @@ def get_all_users_by_the_current_organization_lookup(self) -> list: return api_call def update_organization_user_role_by_user_id(self, user_id: int, role: str): - """The method includes a functionality to update the current organization user by the user id. + """The method includes a functionality to update the current organization user by the user id Args: user_id (int): Specify the id of the user @@ -136,7 +136,7 @@ def update_organization_user_role_by_user_id(self, user_id: int, role: str): raise ValueError def delete_organization_user_by_user_id(self, user_id: int): - """The method includes a functionality to delete the current organization user by the user id. + """The method includes a functionality to delete the current organization user by the user id Args: user_id (int): Specify the id of the user @@ -173,7 +173,7 @@ def delete_organization_user_by_user_id(self, user_id: int): raise ValueError def update_current_organization(self, name: str): - """The method includes a functionality to update the current organization. + """The method includes a functionality to update the current organization Args: name (str): Specify the new name of the current organization @@ -213,7 +213,7 @@ def update_current_organization(self, name: str): def add_new_user_to_current_organization( self, login_or_email: str, role: str ) -> int: - """The method includes a functionality to add a new user to the current organization. + """The method includes a functionality to add a new user to the current organization Args: login_or_email (str): Specify the added user @@ -253,7 +253,7 @@ def add_new_user_to_current_organization( class OrganisationAdmin: - """The class includes all necessary methods to access the Grafana organisation Admin API endpoint. Be aware that all functionalities inside the class only working with basic authentication (username and password). + """The class includes all necessary methods to access the Grafana organisation Admin API endpoint. Be aware that all functionalities inside the class only working with basic authentication (username and password) Args: grafana_api_model (APIModel): Inject a Grafana API model object that includes all necessary values and information @@ -266,7 +266,7 @@ def __init__(self, grafana_api_model: APIModel): self.grafana_api_model = grafana_api_model def get_organization_by_id(self, org_id: int) -> dict: - """The method includes a functionality to get an organization by the id. + """The method includes a functionality to get an organization by the id Args: org_id (int): Specify the organization id @@ -300,7 +300,7 @@ def get_organization_by_id(self, org_id: int) -> dict: raise ValueError def get_organization_by_name(self, name: str) -> dict: - """The method includes a functionality to get an organization by the name. + """The method includes a functionality to get an organization by the name Args: name (str): Specify the organization name @@ -335,7 +335,7 @@ def get_organization_by_name(self, name: str) -> dict: raise ValueError def get_organizations(self) -> list: - """The method includes a functionality to get all organizations. + """The method includes a functionality to get all organizations Required Permissions: Action: orgs:read @@ -362,7 +362,7 @@ def get_organizations(self) -> list: return api_call def create_organization(self, name: str) -> int: - """The method includes a functionality to create an organization. + """The method includes a functionality to create an organization Args: name (str): Specify the organization name @@ -401,7 +401,7 @@ def create_organization(self, name: str) -> int: raise ValueError def update_organization(self, org_id: int, name: str): - """The method includes a functionality to update an organization. + """The method includes a functionality to update an organization Args: org_id (int): Specify the organization id @@ -440,7 +440,7 @@ def update_organization(self, org_id: int, name: str): raise ValueError def delete_organization(self, org_id: int): - """The method includes a functionality to delete an organization. + """The method includes a functionality to delete an organization Args: org_id (int): Specify the organization id @@ -477,7 +477,7 @@ def delete_organization(self, org_id: int): raise ValueError def get_organization_users(self, org_id: int) -> list: - """The method includes a functionality to get all organization users specified by the organization id. + """The method includes a functionality to get all organization users specified by the organization id Args: org_id (int): Specify the organization id @@ -511,7 +511,7 @@ def get_organization_users(self, org_id: int) -> list: raise ValueError def add_organization_user(self, org_id: int, login_or_email: str, role: str) -> int: - """The method includes a functionality to add a user to an organization. + """The method includes a functionality to add a user to an organization Args: org_id (int): Specify the organization id @@ -551,7 +551,7 @@ def add_organization_user(self, org_id: int, login_or_email: str, role: str) -> raise ValueError def update_organization_user(self, org_id: int, user_id: int, role: str): - """The method includes a functionality to update organization user specified by the organization id, the user_id and the role. + """The method includes a functionality to update organization user specified by the organization id, the user_id and the role Args: org_id (int): Specify the organization id @@ -591,7 +591,7 @@ def update_organization_user(self, org_id: int, user_id: int, role: str): raise ValueError def delete_organization_user(self, org_id: int, user_id: int): - """The method includes a functionality to remove an organization users specified by the organization id and the user id. + """The method includes a functionality to remove an organization users specified by the organization id and the user id Args: org_id (int): Specify the organization id diff --git a/src/grafana_api/other_http.py b/src/grafana_api/other_http.py new file mode 100644 index 0000000..188f660 --- /dev/null +++ b/src/grafana_api/other_http.py @@ -0,0 +1,100 @@ +import logging + +import requests + +from .model import APIModel, APIEndpoints +from .api import Api + + +class Organisation: + """The class includes all necessary methods to access the Grafana organisation API endpoint + + Args: + grafana_api_model (APIModel): Inject a Grafana API model object that includes all necessary values and information + + Attributes: + grafana_api_model (APIModel): This is where we store the grafana_api_model + """ + + def __init__(self, grafana_api_model: APIModel): + self.grafana_api_model = grafana_api_model + + def get_frontend_settings(self) -> dict: + """The method includes a functionality to get the frontend settings + + Raises: + Exception: Unspecified error by executing the API call + + Returns: + api_call (dict): Returns the corresponding frontend settings + """ + + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api(f"{APIEndpoints.FRONTEND.value}/settings") + .json() + ) + + if api_call == dict() or api_call.get("id") is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call + + def renew_login_session_based_on_remember_cookie(self) -> dict: + """The method includes a functionality to renew the login session based on the remember cookie + + Raises: + Exception: Unspecified error by executing the API call + + Returns: + None + """ + + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api(f"{APIEndpoints.LOGIN.value}/ping") + .json() + ) + + if api_call.get("message") != "Logged in": + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + logging.info("You successfully destroyed the dashboard snapshot.") + + def get_health_status(self) -> dict: + """The method includes a functionality to get the health information + + Raises: + Exception: Unspecified error by executing the API call + + Returns: + api_call (dict): Returns the health information + """ + + api_call: dict = requests.get(f"{self.grafana_api_model.host}/api/health").json() + + if api_call == dict() or api_call.get("commit") is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call + + def get_metrics(self) -> str: + """The method includes a functionality to get the Grafana metrics information + + Raises: + Exception: Unspecified error by executing the API call + + Returns: + api_call (str): Returns the metrics information + """ + + api_call: str = requests.get(f"{self.grafana_api_model.host}/metrics").text + + if len(api_call) == 0 or api_call is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call diff --git a/src/grafana_api/query_history.py b/src/grafana_api/query_history.py index f2c5e50..39de3cb 100644 --- a/src/grafana_api/query_history.py +++ b/src/grafana_api/query_history.py @@ -5,13 +5,12 @@ APIModel, APIEndpoints, RequestsMethods, - TeamObject, ) from .api import Api class QueryHistory: - """The class includes all necessary methods to access the Grafana query history API endpoints. Be aware that it requires that the user is logged in and that Query history feature is enabled in config file + """The class includes all necessary methods to access the Grafana query history API endpoints. Be aware that it requires that the user is logged in and that query history feature is enabled in the config file Args: grafana_api_model (APIModel): Inject a Grafana API model object that includes all necessary values and information @@ -24,61 +23,238 @@ def __init__(self, grafana_api_model: APIModel): self.grafana_api_model = grafana_api_model def add_query_to_history(self, datasource_uid: str, queries: list) -> dict: - """The method includes a functionality to get the organization teams specified by the optional pagination functionality + """The method includes a functionality to add queries to query history Args: datasource_uid (str): Specify the datasource uid queries (list): Specify the queries as list from type QueryObject Raises: + ValueError: Missed specifying a necessary value Exception: Unspecified error by executing the API call Returns: - api_call (dict): Returns the organization teams + api_call (dict): Returns the added result of the query history """ - api_call: dict = ( - Api(self.grafana_api_model) - .call_the_api( - APIEndpoints.QUERY_HISTORY.value, - RequestsMethods.POST, - json.dumps(dict({"datasourceUid": datasource_uid, "queries": queries})) + if len(datasource_uid) != 0 and len(queries) != 0: + queries_json_list: list = list() + + for query in queries: + query_json_dict: dict = dict( + { + "refId": query.ref_id, + "key": query.key, + "scenarioId": query.scenario_id, + "datasource": dict({"type": query.datasource.type, "uid": query.datasource.uid}), + } + ) + queries_json_list.append(query_json_dict) + + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api( + APIEndpoints.QUERY_HISTORY.value, + RequestsMethods.POST, + json.dumps( + dict({"datasourceUid": datasource_uid, "queries": queries_json_list}) + ), + ) + .json() ) - .json() - ) - if api_call == dict() or api_call.get("result") is None: - logging.error(f"Check the error: {api_call}.") - raise Exception + if api_call == dict() or api_call.get("result") is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call else: - return api_call + logging.error("There is no datasource_uid or queries defined.") + raise ValueError - def search_history_query(self, datasource_uid: str, queries: list) -> dict: - """The method includes a functionality to get the organization teams specified by the optional pagination functionality + def search_query_history( + self, + datasource_uids: list, + search_string: str, + sort: str = "time-desc", + only_starred: bool = False, + pages: int = 1, + results_per_page: int = 100, + ) -> dict: + """The method includes a functionality to search a query inside the query history Args: - datasource_uid (str): Specify the datasource uid - queries (list): Specify the queries as list from type QueryObject + datasource_uids (list): Specify the datasource uid + search_string (str): Specify the search string to filter the result + sort (str): Specify the sorting order e.g. time-asc or time-desc (default time-desc) + only_starred (bool): Specify if queries that are starred should be used for the search (default false) + pages (int): Specify the pages as integer (default 1) + results_per_page (int): Specify the results_per_page as integer (default 100) + + Raises: + ValueError: Missed specifying a necessary value + Exception: Unspecified error by executing the API call + + Returns: + api_call (dict): Returns the corresponding result of the query history + """ + + if len(datasource_uids) != 0 and len(search_string) != 0: + datasource_uids_str: str = "" + + for i in range(0, len(datasource_uids)): + datasource_uids_str = f"{datasource_uids_str}datasourceUid='{datasource_uids[i]}'" + + if len(datasource_uids) != 1 and i != len(datasource_uids) - 1: + datasource_uids_str = f"{datasource_uids_str}&" + + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api( + f"{APIEndpoints.QUERY_HISTORY.value}?{datasource_uids_str}&searchString='{search_string}'&sort='{sort}'&onlyStarred={only_starred}&page={pages}&limit={results_per_page}", + ) + .json() + ) + + if api_call == dict() or api_call.get("result") is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call + else: + logging.error("There is no datasource_uids or search_string defined.") + raise ValueError + + def delete_query_history(self, uid: str): + """The method includes a functionality to delete a query inside the query history + + Args: + uid (str): Specify the uid of the query + + Raises: + ValueError: Missed specifying a necessary value + Exception: Unspecified error by executing the API call + + Returns: + None + """ + + if len(uid) != 0: + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api( + f"{APIEndpoints.QUERY_HISTORY.value}/{uid}", RequestsMethods.DELETE + ) + .json() + ) + + if api_call.get("message") != "Query deleted": + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + logging.info("You successfully deleted the query.") + else: + logging.error("There is no uid defined.") + raise ValueError + + def update_query_history(self, uid: str, comment: str) -> dict: + """The method includes a functionality to update a query inside the query history + + Args: + uid (str): Specify the uid of the query + comment (str): Specify the comment of the query + + Raises: + ValueError: Missed specifying a necessary value + Exception: Unspecified error by executing the API call + + Returns: + api_call (dict): Returns the modified result of the query history + """ + + if len(uid) != 0 and len(comment) != 0: + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api( + f"{APIEndpoints.QUERY_HISTORY.value}/{uid}", + RequestsMethods.PATCH, + json.dumps({"comment": comment}), + ) + .json() + ) + + if api_call == dict() or api_call.get("result") is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call + else: + logging.error("There is no uid or comment defined.") + raise ValueError + + def star_query_history(self, uid: str) -> dict: + """The method includes a functionality to star a query inside the query history + + Args: + uid (str): Specify the uid of the query + + Raises: + ValueError: Missed specifying a necessary value + Exception: Unspecified error by executing the API call + + Returns: + api_call (dict): Returns the corresponding stared query history + """ + + if len(uid) != 0: + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api( + f"{APIEndpoints.QUERY_HISTORY.value}/star/{uid}", + RequestsMethods.POST, + json.dumps({}), + ) + .json() + ) + + if api_call == dict() or api_call.get("result") is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call + else: + logging.error("There is no uid defined.") + raise ValueError + + def unstar_query_history(self, uid: str) -> dict: + """The method includes a functionality to unstar a query inside the query history + + Args: + uid (str): Specify the uid of the query Raises: + ValueError: Missed specifying a necessary value Exception: Unspecified error by executing the API call Returns: - api_call (dict): Returns the organization teams + api_call (dict): Returns the corresponding unstared query history """ - api_call: dict = ( - Api(self.grafana_api_model) - .call_the_api( - APIEndpoints.QUERY_HISTORY.value, - RequestsMethods.POST, - json.dumps(dict({"datasourceUid": datasource_uid, "queries": queries})) + if len(uid) != 0: + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api( + f"{APIEndpoints.QUERY_HISTORY.value}/star/{uid}", + RequestsMethods.DELETE, + ) + .json() ) - .json() - ) - if api_call == dict() or api_call.get("result") is None: - logging.error(f"Check the error: {api_call}.") - raise Exception + if api_call == dict() or api_call.get("result") is None: + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + return api_call else: - return api_call \ No newline at end of file + logging.error("There is no uid defined.") + raise ValueError diff --git a/src/grafana_api/reporting.py b/src/grafana_api/reporting.py new file mode 100644 index 0000000..3a52ae6 --- /dev/null +++ b/src/grafana_api/reporting.py @@ -0,0 +1,71 @@ +import json +import logging + +from .model import ( + APIModel, + APIEndpoints, + RequestsMethods, +) +from .api import Api + + +class Reporting: + """The class includes all necessary methods to access the Grafana reporting API endpoints. Be aware that the functionality is a Grafana ENTERPRISE v7.0+ feature + + HINT: Note Grafana Enterprise API need required permissions if fine-grained access control is enabled + + Args: + grafana_api_model (APIModel): Inject a Grafana API model object that includes all necessary values and information + + Attributes: + grafana_api_model (APIModel): This is where we store the grafana_api_model + """ + + def __init__(self, grafana_api_model: APIModel): + self.grafana_api_model = grafana_api_model + + def send_report(self, id: int, emails: str = None, use_emails_from_report: bool = None): + """The method includes a functionality to send a report to a specified email addresses + + Args: + id (int): Specify the id of sented report. It is the same as in the URL when editing a report, not to be confused with the id of the dashboard. + emails (str): Specify the comma-separated list of emails to which to send the report to. Overrides the emails from the report. Required if useEmailsFromReport is not present (default None) + use_emails_from_report (bool): Specify the if the emails inside the report should be used. Required if emails is not present (default None) + + Required Permissions: + Action: reports:send + Scope: N/A + + Raises: + ValueError: Missed specifying a necessary value + Exception: Unspecified error by executing the API call + + Returns: + None + """ + + if id != 0 and (emails is not None and (len(emails) != 0) or (use_emails_from_report is not None and use_emails_from_report)): + if emails is not None and len(emails) != 0: + result: dict = dict({"id": id, "emails": emails}) + else: + result: dict = dict({"id": id, "useEmailsFromReport": use_emails_from_report}) + + api_call: dict = ( + Api(self.grafana_api_model) + .call_the_api( + APIEndpoints.REPORTING.value, + RequestsMethods.POST, + json.dumps(result), + timeout=60 + ) + .json() + ) + + if api_call.get("message") != "Report was sent": + logging.error(f"Check the error: {api_call}.") + raise Exception + else: + logging.info("You successfully send the report.") + else: + logging.error("There is no id, emails or useEmailsFromReport defined.") + raise ValueError diff --git a/tests/integrationtest/query_history.py b/tests/integrationtest/test_other_http.py similarity index 100% rename from tests/integrationtest/query_history.py rename to tests/integrationtest/test_other_http.py diff --git a/tests/integrationtest/test_query_history.py b/tests/integrationtest/test_query_history.py new file mode 100644 index 0000000..e2364eb --- /dev/null +++ b/tests/integrationtest/test_query_history.py @@ -0,0 +1,33 @@ +import os +from unittest import TestCase + +from src.grafana_api.model import ( + APIModel, + QueryObject, + QueryDatasourceObject, +) +from src.grafana_api.datasource import Datasource +from src.grafana_api.query_history import QueryHistory + + +class QueryHistoryTest(TestCase): + model: APIModel = APIModel( + host=os.environ["GRAFANA_HOST"], + token=os.environ["GRAFANA_TOKEN"], + ) + data_source: Datasource = Datasource(model) + query_history: QueryHistory = QueryHistory(model) + + def test_search_query_history(self): + self.assertEqual(0, self.query_history.search_query_history(list(["5yBH2Yxnk"]), "logs").get("result").get("totalCount")) + + def test_add_query_to_history(self): + query_datasource: QueryDatasourceObject = QueryDatasourceObject("testdata", "5yBH2Yxnk") + query: QueryObject = QueryObject("A", "test", "logs", query_datasource) + + self.query_history.add_query_to_history( + datasource_uid="5yBH2Yxnk", + queries=list([query]) + ) + + print(self.query_history.search_query_history(list(["5yBH2Yxnk"]), "logs")) \ No newline at end of file diff --git a/tests/unittests/test_licensing.py b/tests/unittests/test_licensing.py new file mode 100644 index 0000000..72bbd14 --- /dev/null +++ b/tests/unittests/test_licensing.py @@ -0,0 +1,74 @@ +from unittest import TestCase +from unittest.mock import MagicMock, Mock, patch + +from src.grafana_api.model import APIModel +from src.grafana_api.licensing import Licensing + + +class LicenseTestCase(TestCase): + @patch("src.grafana_api.api.Api.call_the_api") + def test_check_license_availability(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + licensing: Licensing = Licensing(grafana_api_model=model) + + call_the_api_mock.return_value.status_code = 200 + call_the_api_mock.return_value.text = "true" + + self.assertEqual(True, licensing.check_license_availability()) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_check_license_availability_no_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + licensing: Licensing = Licensing(grafana_api_model=model) + + call_the_api_mock.return_value.status_code = 400 + + with self.assertRaises(Exception): + licensing.check_license_availability() + + @patch("src.grafana_api.api.Api.call_the_api") + def test_manually_force_license_refresh(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + licensing: Licensing = Licensing(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"jti": "2"})) + + call_the_api_mock.return_value = mock + + self.assertEqual(dict({"jti": "2"}), licensing.manually_force_license_refresh()) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_manually_force_license_refresh_no_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + licensing: Licensing = Licensing(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + licensing.manually_force_license_refresh() + + @patch("src.grafana_api.api.Api.call_the_api") + def test_remove_license_from_dashboard(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + licensing: Licensing = Licensing(grafana_api_model=model) + + call_the_api_mock.return_value.status_code = 200 + + self.assertEqual(None, licensing.remove_license_from_dashboard()) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_remove_license_from_dashboard_no_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + licensing: Licensing = Licensing(grafana_api_model=model) + + mock: Mock = Mock() + mock.return_value.json = dict() + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + licensing.remove_license_from_dashboard() diff --git a/tests/unittests/query_history.py b/tests/unittests/test_other_http.py similarity index 100% rename from tests/unittests/query_history.py rename to tests/unittests/test_other_http.py diff --git a/tests/unittests/test_query_history.py b/tests/unittests/test_query_history.py new file mode 100644 index 0000000..158e8d8 --- /dev/null +++ b/tests/unittests/test_query_history.py @@ -0,0 +1,241 @@ +from unittest import TestCase +from unittest.mock import MagicMock, Mock, patch + +from src.grafana_api.model import APIModel, QueryObject, QueryDatasourceObject +from src.grafana_api.query_history import QueryHistory + + +class QueryHistoryTestCase(TestCase): + @patch("src.grafana_api.api.Api.call_the_api") + def test_add_query_to_history(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + query_datasource: QueryDatasourceObject = QueryDatasourceObject("test", "test") + query: QueryObject = QueryObject("test", "test", "test", query_datasource) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"result": "test"})) + + call_the_api_mock.return_value = mock + + self.assertEqual(dict({"result": "test"}), query_history.add_query_to_history("test", [query])) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_add_query_to_history_no_datasource_uid(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(ValueError): + query_history.add_query_to_history("", []) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_add_query_to_history_no_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + query_datasource: QueryDatasourceObject = QueryDatasourceObject("test", "test") + query: QueryObject = QueryObject("test", "test", "test", query_datasource) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + query_history.add_query_to_history("test", [query]) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_search_query_history(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"result": "test"})) + + call_the_api_mock.return_value = mock + + self.assertEqual(dict({"result": "test"}), query_history.search_query_history(["test", "test"], "test")) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_search_query_history_no_datasource_uids(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(ValueError): + query_history.search_query_history([], "") + + @patch("src.grafana_api.api.Api.call_the_api") + def test_search_query_history_no_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + query_history.search_query_history(["test"], "test") + + @patch("src.grafana_api.api.Api.call_the_api") + def test_delete_query_history(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"message": "Query deleted"})) + + call_the_api_mock.return_value = mock + + self.assertEqual(None, query_history.delete_query_history("test")) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_delete_query_history_no_datasource_uid(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(ValueError): + query_history.delete_query_history("") + + @patch("src.grafana_api.api.Api.call_the_api") + def test_delete_query_history_no_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"message": "test"})) + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + query_history.delete_query_history("test") + + @patch("src.grafana_api.api.Api.call_the_api") + def test_update_query_history(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"result": "test"})) + + call_the_api_mock.return_value = mock + + self.assertEqual(dict({"result": "test"}), query_history.update_query_history("test", "test")) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_update_query_history_no_uid(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(ValueError): + query_history.update_query_history("", "") + + @patch("src.grafana_api.api.Api.call_the_api") + def test_update_query_history_no_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + query_history.update_query_history("test", "test") + + @patch("src.grafana_api.api.Api.call_the_api") + def test_star_query_history(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"result": "test"})) + + call_the_api_mock.return_value = mock + + self.assertEqual(dict({"result": "test"}), query_history.star_query_history("test")) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_star_query_history_no_uid(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(ValueError): + query_history.star_query_history("") + + @patch("src.grafana_api.api.Api.call_the_api") + def test_star_query_history_no_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + query_history.star_query_history("test") + + @patch("src.grafana_api.api.Api.call_the_api") + def test_unstar_query_history(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"result": "test"})) + + call_the_api_mock.return_value = mock + + self.assertEqual(dict({"result": "test"}), query_history.unstar_query_history("test")) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_unstar_query_history_no_uid(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(ValueError): + query_history.unstar_query_history("") + + @patch("src.grafana_api.api.Api.call_the_api") + def test_unstar_query_history_no_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + query_history: QueryHistory = QueryHistory(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + query_history.unstar_query_history("test") diff --git a/tests/unittests/test_reporting.py b/tests/unittests/test_reporting.py new file mode 100644 index 0000000..a47d3cd --- /dev/null +++ b/tests/unittests/test_reporting.py @@ -0,0 +1,45 @@ +from unittest import TestCase +from unittest.mock import MagicMock, Mock, patch + +from src.grafana_api.model import APIModel +from src.grafana_api.reporting import Reporting + + +class ReportingTestCase(TestCase): + @patch("src.grafana_api.api.Api.call_the_api") + def test_send_report(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + reporting: Reporting = Reporting(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"message": "Report was sent"})) + + call_the_api_mock.return_value = mock + + self.assertEqual(None, reporting.send_report(1, emails="test,test")) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_send_report_no_id(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + reporting: Reporting = Reporting(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(ValueError): + reporting.send_report(0, emails="") + + @patch("src.grafana_api.api.Api.call_the_api") + def test_send_report_no_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + reporting: Reporting = Reporting(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"message": "Test"})) + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + reporting.send_report(1, use_emails_from_report=True) From e4b52a3da40dda59afa0f07df9da723b2e1c7b96 Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Fri, 27 May 2022 13:27:49 +0200 Subject: [PATCH 3/5] WIP --- src/grafana_api/other_http.py | 8 +- tests/integrationtest/test_other_http.py | 24 +++++ tests/unittests/test_other_http.py | 107 +++++++++++++++++++++++ 3 files changed, 135 insertions(+), 4 deletions(-) diff --git a/src/grafana_api/other_http.py b/src/grafana_api/other_http.py index 188f660..418d3e2 100644 --- a/src/grafana_api/other_http.py +++ b/src/grafana_api/other_http.py @@ -6,8 +6,8 @@ from .api import Api -class Organisation: - """The class includes all necessary methods to access the Grafana organisation API endpoint +class OtherHTTP: + """The class includes all necessary methods to access other Grafana API endpoints Args: grafana_api_model (APIModel): Inject a Grafana API model object that includes all necessary values and information @@ -35,13 +35,13 @@ def get_frontend_settings(self) -> dict: .json() ) - if api_call == dict() or api_call.get("id") is None: + if api_call == dict(): logging.error(f"Check the error: {api_call}.") raise Exception else: return api_call - def renew_login_session_based_on_remember_cookie(self) -> dict: + def renew_login_session_based_on_remember_cookie(self): """The method includes a functionality to renew the login session based on the remember cookie Raises: diff --git a/tests/integrationtest/test_other_http.py b/tests/integrationtest/test_other_http.py index e69de29..bfc7960 100644 --- a/tests/integrationtest/test_other_http.py +++ b/tests/integrationtest/test_other_http.py @@ -0,0 +1,24 @@ +import os +from unittest import TestCase + +from src.grafana_api.model import ( + APIModel, +) +from src.grafana_api.other_http import OtherHTTP + + +class OtherHTTPTest(TestCase): + model: APIModel = APIModel( + host=os.environ["GRAFANA_HOST"], + token=os.environ["GRAFANA_TOKEN"], + ) + other_http: OtherHTTP = OtherHTTP(model) + + def test_get_frontend_settings(self): + self.assertEqual(False, self.other_http.get_frontend_settings().get("allowOrgCreate")) + + def test_renew_login_session_based_on_remember_cookie(self): + self.assertIsNone(self.other_http.renew_login_session_based_on_remember_cookie()) + + def test_get_health_status(self): + self.assertIsNotNone(self.other_http.get_health_status()) diff --git a/tests/unittests/test_other_http.py b/tests/unittests/test_other_http.py index e69de29..da31e1d 100644 --- a/tests/unittests/test_other_http.py +++ b/tests/unittests/test_other_http.py @@ -0,0 +1,107 @@ +from unittest import TestCase +from unittest.mock import MagicMock, Mock, patch + +from src.grafana_api.model import APIModel +from src.grafana_api.other_http import OtherHTTP + + +class OtherHTTPTestCase(TestCase): + @patch("src.grafana_api.api.Api.call_the_api") + def test_get_frontend_settings(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + other_http: OtherHTTP = OtherHTTP(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"allowOrgCreate": True})) + + call_the_api_mock.return_value = mock + + self.assertEqual(dict({"allowOrgCreate": True}), other_http.get_frontend_settings()) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_get_frontend_settings_no_valid_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + other_http: OtherHTTP = OtherHTTP(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + other_http.get_frontend_settings() + + @patch("src.grafana_api.api.Api.call_the_api") + def test_renew_login_session_based_on_remember_cookie(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + other_http: OtherHTTP = OtherHTTP(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"message": "Logged in"})) + + call_the_api_mock.return_value = mock + + self.assertEqual(None, other_http.renew_login_session_based_on_remember_cookie()) + + @patch("src.grafana_api.api.Api.call_the_api") + def test_renew_login_session_based_on_remember_cookie_no_valid_result(self, call_the_api_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + other_http: OtherHTTP = OtherHTTP(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + call_the_api_mock.return_value = mock + + with self.assertRaises(Exception): + other_http.renew_login_session_based_on_remember_cookie() + + @patch("requests.get") + def test_get_health_status(self, get_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + other_http: OtherHTTP = OtherHTTP(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict({"commit": "087143285"})) + + get_mock.return_value = mock + + self.assertEqual(dict({"commit": "087143285"}), other_http.get_health_status()) + + @patch("requests.get") + def test_get_health_status_no_valid_result(self, get_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + other_http: OtherHTTP = OtherHTTP(grafana_api_model=model) + + mock: Mock = Mock() + mock.json = Mock(return_value=dict()) + + get_mock.return_value = mock + + with self.assertRaises(Exception): + other_http.get_health_status() + + @patch("requests.get") + def test_get_metrics(self, get_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + other_http: OtherHTTP = OtherHTTP(grafana_api_model=model) + + mock: Mock = Mock() + mock.text = "test" + + get_mock.return_value = mock + + self.assertEqual("test", other_http.get_metrics()) + + @patch("requests.get") + def test_get_metrics_no_valid_result(self, get_mock): + model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) + other_http: OtherHTTP = OtherHTTP(grafana_api_model=model) + + mock: Mock = Mock() + mock.text = "" + + get_mock.return_value = mock + + with self.assertRaises(Exception): + other_http.get_metrics() From eb6f9f7b63045a5c26a250bba674ebef90ffe9c0 Mon Sep 17 00:00:00 2001 From: Pascal Zimmermann Date: Mon, 30 May 2022 23:56:19 +0200 Subject: [PATCH 4/5] Add the corresponding support --- src/grafana_api/api.py | 54 +++++++++++++++++---- src/grafana_api/licensing.py | 20 +++----- src/grafana_api/other_http.py | 10 ++-- src/grafana_api/query_history.py | 15 ++++-- src/grafana_api/reporting.py | 16 ++++-- tests/integrationtest/test_other_http.py | 8 ++- tests/integrationtest/test_query_history.py | 16 ++++-- tests/unittests/test_api.py | 30 ++++++++---- tests/unittests/test_other_http.py | 14 ++++-- tests/unittests/test_query_history.py | 22 +++++++-- 10 files changed, 147 insertions(+), 58 deletions(-) diff --git a/src/grafana_api/api.py b/src/grafana_api/api.py index dfb213d..bfaaff6 100644 --- a/src/grafana_api/api.py +++ b/src/grafana_api/api.py @@ -1,4 +1,5 @@ import logging +import json import requests @@ -65,7 +66,12 @@ def call_the_api( elif method.value == RequestsMethods.PUT.value: if json_complete is not None: return Api.__check_the_api_call_response( - requests.put(api_url, data=json_complete, headers=headers, timeout=timeout) + requests.put( + api_url, + data=json_complete, + headers=headers, + timeout=timeout, + ) ) else: logging.error("Please define the json_complete.") @@ -73,7 +79,12 @@ def call_the_api( elif method.value == RequestsMethods.POST.value: if json_complete is not None: return Api.__check_the_api_call_response( - requests.post(api_url, data=json_complete, headers=headers, timeout=timeout) + requests.post( + api_url, + data=json_complete, + headers=headers, + timeout=timeout, + ) ) else: logging.error("Please define the json_complete.") @@ -81,7 +92,12 @@ def call_the_api( elif method.value == RequestsMethods.PATCH.value: if json_complete is not None: return Api.__check_the_api_call_response( - requests.patch(api_url, data=json_complete, headers=headers, timeout=timeout) + requests.patch( + api_url, + data=json_complete, + headers=headers, + timeout=timeout, + ) ) else: logging.error("Please define the json_complete.") @@ -110,12 +126,30 @@ def __check_the_api_call_response(response: any = None) -> any: api_call (any): Returns the value of the api call """ - if len(response.text) != 0 and type(response.json()) == dict: - if ( - "message" in response.json().keys() - and response.json()["message"] in ERROR_MESSAGES - ): - logging.error(response.json()["message"]) - raise requests.exceptions.ConnectionError + if Api.__check_if_valid_json(response.text): + if len(response.text) != 0 and type(response.json()) == dict: + if ( + "message" in response.json().keys() + and response.json()["message"] in ERROR_MESSAGES + ): + logging.error(response.json()["message"]) + raise requests.exceptions.ConnectionError return response + + @staticmethod + def __check_if_valid_json(response: any) -> bool: + """The method includes a functionality to check if the response json is valid + + Args: + response (any): Specify the inserted response json + + Returns: + api_call (bool): Returns if the json is valid or not + """ + + try: + json.loads(response) + except ValueError: + return False + return True diff --git a/src/grafana_api/licensing.py b/src/grafana_api/licensing.py index 0c15eff..b0a7234 100644 --- a/src/grafana_api/licensing.py +++ b/src/grafana_api/licensing.py @@ -39,11 +39,8 @@ def check_license_availability(self): api_call (bool): Returns the result if the license is available or not """ - api_call: Response = ( - Api(self.grafana_api_model) - .call_the_api( - f"{APIEndpoints.LICENSING.value}/check", - ) + api_call: Response = Api(self.grafana_api_model).call_the_api( + f"{APIEndpoints.LICENSING.value}/check", ) if api_call.status_code != 200: @@ -96,16 +93,15 @@ def remove_license_from_dashboard(self): api_call (dict): Returns the result of license refresh call """ - api_call: Response = ( - Api(self.grafana_api_model) - .call_the_api( - f"{APIEndpoints.LICENSING.value}/token", - RequestsMethods.DELETE, - ) + api_call: Response = Api(self.grafana_api_model).call_the_api( + f"{APIEndpoints.LICENSING.value}/token", + RequestsMethods.DELETE, ) if api_call.status_code != 200: logging.error(f"Check the error: {api_call}.") raise Exception else: - logging.info("You successfully removed the corresponding license from the database.") + logging.info( + "You successfully removed the corresponding license from the database." + ) diff --git a/src/grafana_api/other_http.py b/src/grafana_api/other_http.py index 418d3e2..cd40ec7 100644 --- a/src/grafana_api/other_http.py +++ b/src/grafana_api/other_http.py @@ -51,13 +51,13 @@ def renew_login_session_based_on_remember_cookie(self): None """ - api_call: dict = ( + api_call: str = ( Api(self.grafana_api_model) .call_the_api(f"{APIEndpoints.LOGIN.value}/ping") - .json() + .text ) - if api_call.get("message") != "Logged in": + if api_call != "Logged in": logging.error(f"Check the error: {api_call}.") raise Exception else: @@ -73,7 +73,9 @@ def get_health_status(self) -> dict: api_call (dict): Returns the health information """ - api_call: dict = requests.get(f"{self.grafana_api_model.host}/api/health").json() + api_call: dict = requests.get( + f"{self.grafana_api_model.host}/api/health" + ).json() if api_call == dict() or api_call.get("commit") is None: logging.error(f"Check the error: {api_call}.") diff --git a/src/grafana_api/query_history.py b/src/grafana_api/query_history.py index 39de3cb..6dfd0cf 100644 --- a/src/grafana_api/query_history.py +++ b/src/grafana_api/query_history.py @@ -46,7 +46,9 @@ def add_query_to_history(self, datasource_uid: str, queries: list) -> dict: "refId": query.ref_id, "key": query.key, "scenarioId": query.scenario_id, - "datasource": dict({"type": query.datasource.type, "uid": query.datasource.uid}), + "datasource": dict( + {"type": query.datasource.type, "uid": query.datasource.uid} + ), } ) queries_json_list.append(query_json_dict) @@ -57,7 +59,12 @@ def add_query_to_history(self, datasource_uid: str, queries: list) -> dict: APIEndpoints.QUERY_HISTORY.value, RequestsMethods.POST, json.dumps( - dict({"datasourceUid": datasource_uid, "queries": queries_json_list}) + dict( + { + "datasourceUid": datasource_uid, + "queries": queries_json_list, + } + ) ), ) .json() @@ -103,7 +110,9 @@ def search_query_history( datasource_uids_str: str = "" for i in range(0, len(datasource_uids)): - datasource_uids_str = f"{datasource_uids_str}datasourceUid='{datasource_uids[i]}'" + datasource_uids_str = ( + f"{datasource_uids_str}datasourceUid='{datasource_uids[i]}'" + ) if len(datasource_uids) != 1 and i != len(datasource_uids) - 1: datasource_uids_str = f"{datasource_uids_str}&" diff --git a/src/grafana_api/reporting.py b/src/grafana_api/reporting.py index 3a52ae6..5056fbe 100644 --- a/src/grafana_api/reporting.py +++ b/src/grafana_api/reporting.py @@ -24,7 +24,9 @@ class Reporting: def __init__(self, grafana_api_model: APIModel): self.grafana_api_model = grafana_api_model - def send_report(self, id: int, emails: str = None, use_emails_from_report: bool = None): + def send_report( + self, id: int, emails: str = None, use_emails_from_report: bool = None + ): """The method includes a functionality to send a report to a specified email addresses Args: @@ -44,11 +46,17 @@ def send_report(self, id: int, emails: str = None, use_emails_from_report: bool None """ - if id != 0 and (emails is not None and (len(emails) != 0) or (use_emails_from_report is not None and use_emails_from_report)): + if id != 0 and ( + emails is not None + and (len(emails) != 0) + or (use_emails_from_report is not None and use_emails_from_report) + ): if emails is not None and len(emails) != 0: result: dict = dict({"id": id, "emails": emails}) else: - result: dict = dict({"id": id, "useEmailsFromReport": use_emails_from_report}) + result: dict = dict( + {"id": id, "useEmailsFromReport": use_emails_from_report} + ) api_call: dict = ( Api(self.grafana_api_model) @@ -56,7 +64,7 @@ def send_report(self, id: int, emails: str = None, use_emails_from_report: bool APIEndpoints.REPORTING.value, RequestsMethods.POST, json.dumps(result), - timeout=60 + timeout=60, ) .json() ) diff --git a/tests/integrationtest/test_other_http.py b/tests/integrationtest/test_other_http.py index bfc7960..ff3d813 100644 --- a/tests/integrationtest/test_other_http.py +++ b/tests/integrationtest/test_other_http.py @@ -15,10 +15,14 @@ class OtherHTTPTest(TestCase): other_http: OtherHTTP = OtherHTTP(model) def test_get_frontend_settings(self): - self.assertEqual(False, self.other_http.get_frontend_settings().get("allowOrgCreate")) + self.assertEqual( + False, self.other_http.get_frontend_settings().get("allowOrgCreate") + ) def test_renew_login_session_based_on_remember_cookie(self): - self.assertIsNone(self.other_http.renew_login_session_based_on_remember_cookie()) + self.assertIsNone( + self.other_http.renew_login_session_based_on_remember_cookie() + ) def test_get_health_status(self): self.assertIsNotNone(self.other_http.get_health_status()) diff --git a/tests/integrationtest/test_query_history.py b/tests/integrationtest/test_query_history.py index e2364eb..729bac9 100644 --- a/tests/integrationtest/test_query_history.py +++ b/tests/integrationtest/test_query_history.py @@ -19,15 +19,21 @@ class QueryHistoryTest(TestCase): query_history: QueryHistory = QueryHistory(model) def test_search_query_history(self): - self.assertEqual(0, self.query_history.search_query_history(list(["5yBH2Yxnk"]), "logs").get("result").get("totalCount")) + self.assertEqual( + 0, + self.query_history.search_query_history(list(["5yBH2Yxnk"]), "logs") + .get("result") + .get("totalCount"), + ) def test_add_query_to_history(self): - query_datasource: QueryDatasourceObject = QueryDatasourceObject("testdata", "5yBH2Yxnk") + query_datasource: QueryDatasourceObject = QueryDatasourceObject( + "testdata", "5yBH2Yxnk" + ) query: QueryObject = QueryObject("A", "test", "logs", query_datasource) self.query_history.add_query_to_history( - datasource_uid="5yBH2Yxnk", - queries=list([query]) + datasource_uid="5yBH2Yxnk", queries=list([query]) ) - print(self.query_history.search_query_history(list(["5yBH2Yxnk"]), "logs")) \ No newline at end of file + self.query_history.search_query_history(list(["5yBH2Yxnk"]), "logs") diff --git a/tests/unittests/test_api.py b/tests/unittests/test_api.py index a3d38ec..9876d08 100644 --- a/tests/unittests/test_api.py +++ b/tests/unittests/test_api.py @@ -30,7 +30,7 @@ def test_call_the_api_basic_auth(self, get_mock): mock: Mock = Mock() mock.json = Mock(return_value={"status": "success"}) - mock.text = str({"status": "success"}) + mock.text = str('{"status": "success"}') get_mock.return_value = mock @@ -43,7 +43,7 @@ def test_call_the_api_basic_auth(self, get_mock): def test_call_the_api_get_valid(self, get_mock): mock: Mock = Mock() mock.json = Mock(return_value={"status": "success"}) - mock.text = str({"status": "success"}) + mock.text = str('{"status": "success"}') get_mock.return_value = mock @@ -60,7 +60,7 @@ def test_call_the_api_get_not_valid(self): def test_call_the_api_put_valid(self, put_mock): mock: Mock = Mock() mock.json = Mock(return_value={"status": "success"}) - mock.text = str({"status": "success"}) + mock.text = str('{"status": "success"}') put_mock.return_value = mock @@ -81,7 +81,7 @@ def test_call_the_api_put_not_valid(self): def test_call_the_api_post_valid(self, post_mock): mock: Mock = Mock() mock.json = Mock(return_value={"status": "success"}) - mock.text = str({"status": "success"}) + mock.text = str('{"status": "success"}') post_mock.return_value = mock @@ -110,7 +110,7 @@ def test_call_the_api_post_no_data(self): def test_call_the_api_patch_valid(self, post_mock): mock: Mock = Mock() mock.json = Mock(return_value={"status": "success"}) - mock.text = str({"status": "success"}) + mock.text = str('{"status": "success"}') post_mock.return_value = mock @@ -139,7 +139,7 @@ def test_call_the_api_patch_no_data(self): def test_call_the_api_delete_valid(self, delete_mock): mock: Mock = Mock() mock.json = Mock(return_value={"message": "Deletion successful"}) - mock.text = str({"message": "Deletion successful"}) + mock.text = str('{"message": "Deletion successful"}') delete_mock.return_value = mock @@ -157,7 +157,7 @@ def test_call_the_api_delete_not_valid(self): def test_check_the_api_call_response(self): mock: Mock = Mock() mock.json = Mock(return_value=dict({"test": "test"})) - mock.text = str({"test": "test"}) + mock.text = str('{"test": "test"}') self.assertEqual( dict({"test": "test"}), @@ -167,7 +167,7 @@ def test_check_the_api_call_response(self): def test_check_the_api_call_response_no_error_message(self): mock: Mock = Mock() mock.json = Mock(return_value=dict({"message": "test"})) - mock.text = str({"message": "test"}) + mock.text = str('{"message": "test"}') self.assertEqual( dict({"message": "test"}), @@ -185,7 +185,19 @@ def test_check_the_api_call_response_no_json_response_value(self): def test_check_the_api_call_response_exception(self): mock: Mock = Mock() mock.json = Mock(return_value=dict({"message": "invalid API key"})) - mock.text = str({"message": "invalid API key"}) + mock.text = str('{"message": "invalid API key"}') with self.assertRaises(requests.exceptions.ConnectionError): self.api._Api__check_the_api_call_response(response=mock) + + @patch("src.grafana_api.api.Api._Api__check_if_valid_json") + def test__check_the_api_call_response_valid_json(self, check_if_valid_json_mock): + check_if_valid_json_mock.return_value = True + + mock: Mock = Mock() + mock.json = Mock(return_value=str("")) + mock.text = str("") + + self.assertEqual( + "", self.api._Api__check_the_api_call_response(response=mock).text + ) diff --git a/tests/unittests/test_other_http.py b/tests/unittests/test_other_http.py index da31e1d..b93f778 100644 --- a/tests/unittests/test_other_http.py +++ b/tests/unittests/test_other_http.py @@ -16,7 +16,9 @@ def test_get_frontend_settings(self, call_the_api_mock): call_the_api_mock.return_value = mock - self.assertEqual(dict({"allowOrgCreate": True}), other_http.get_frontend_settings()) + self.assertEqual( + dict({"allowOrgCreate": True}), other_http.get_frontend_settings() + ) @patch("src.grafana_api.api.Api.call_the_api") def test_get_frontend_settings_no_valid_result(self, call_the_api_mock): @@ -37,14 +39,18 @@ def test_renew_login_session_based_on_remember_cookie(self, call_the_api_mock): other_http: OtherHTTP = OtherHTTP(grafana_api_model=model) mock: Mock = Mock() - mock.json = Mock(return_value=dict({"message": "Logged in"})) + mock.text = "Logged in" call_the_api_mock.return_value = mock - self.assertEqual(None, other_http.renew_login_session_based_on_remember_cookie()) + self.assertEqual( + None, other_http.renew_login_session_based_on_remember_cookie() + ) @patch("src.grafana_api.api.Api.call_the_api") - def test_renew_login_session_based_on_remember_cookie_no_valid_result(self, call_the_api_mock): + def test_renew_login_session_based_on_remember_cookie_no_valid_result( + self, call_the_api_mock + ): model: APIModel = APIModel(host=MagicMock(), token=MagicMock()) other_http: OtherHTTP = OtherHTTP(grafana_api_model=model) diff --git a/tests/unittests/test_query_history.py b/tests/unittests/test_query_history.py index 158e8d8..b5819cb 100644 --- a/tests/unittests/test_query_history.py +++ b/tests/unittests/test_query_history.py @@ -19,7 +19,10 @@ def test_add_query_to_history(self, call_the_api_mock): call_the_api_mock.return_value = mock - self.assertEqual(dict({"result": "test"}), query_history.add_query_to_history("test", [query])) + self.assertEqual( + dict({"result": "test"}), + query_history.add_query_to_history("test", [query]), + ) @patch("src.grafana_api.api.Api.call_the_api") def test_add_query_to_history_no_datasource_uid(self, call_the_api_mock): @@ -60,7 +63,10 @@ def test_search_query_history(self, call_the_api_mock): call_the_api_mock.return_value = mock - self.assertEqual(dict({"result": "test"}), query_history.search_query_history(["test", "test"], "test")) + self.assertEqual( + dict({"result": "test"}), + query_history.search_query_history(["test", "test"], "test"), + ) @patch("src.grafana_api.api.Api.call_the_api") def test_search_query_history_no_datasource_uids(self, call_the_api_mock): @@ -136,7 +142,9 @@ def test_update_query_history(self, call_the_api_mock): call_the_api_mock.return_value = mock - self.assertEqual(dict({"result": "test"}), query_history.update_query_history("test", "test")) + self.assertEqual( + dict({"result": "test"}), query_history.update_query_history("test", "test") + ) @patch("src.grafana_api.api.Api.call_the_api") def test_update_query_history_no_uid(self, call_the_api_mock): @@ -174,7 +182,9 @@ def test_star_query_history(self, call_the_api_mock): call_the_api_mock.return_value = mock - self.assertEqual(dict({"result": "test"}), query_history.star_query_history("test")) + self.assertEqual( + dict({"result": "test"}), query_history.star_query_history("test") + ) @patch("src.grafana_api.api.Api.call_the_api") def test_star_query_history_no_uid(self, call_the_api_mock): @@ -212,7 +222,9 @@ def test_unstar_query_history(self, call_the_api_mock): call_the_api_mock.return_value = mock - self.assertEqual(dict({"result": "test"}), query_history.unstar_query_history("test")) + self.assertEqual( + dict({"result": "test"}), query_history.unstar_query_history("test") + ) @patch("src.grafana_api.api.Api.call_the_api") def test_unstar_query_history_no_uid(self, call_the_api_mock): From 9681df355c3b941e40b568c5749322a84c75b25c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 30 May 2022 21:57:58 +0000 Subject: [PATCH 5/5] Add coverage badge and documentation --- docs/content/grafana_api/api.md | 4 +++- docs/content/grafana_api/model.md | 34 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/content/grafana_api/api.md b/docs/content/grafana_api/api.md index e820a33..1b8474b 100644 --- a/docs/content/grafana_api/api.md +++ b/docs/content/grafana_api/api.md @@ -34,7 +34,8 @@ The class includes all necessary methods to make API calls to the Grafana API en ```python def call_the_api(api_call: str, method: RequestsMethods = RequestsMethods.GET, - json_complete: str = None) -> any + json_complete: str = None, + timeout: float = None) -> any ``` The method execute a defined API call against the Grafana endpoints @@ -44,6 +45,7 @@ The method execute a defined API call against the Grafana endpoints - `api_call` _str_ - Specify the API call endpoint - `method` _RequestsMethods_ - Specify the used method (default GET) - `json_complete` _str_ - Specify the inserted JSON as string +- `timeout` _float_ - Specify the timeout for the corresponding API call **Raises**: diff --git a/docs/content/grafana_api/model.md b/docs/content/grafana_api/model.md index 061cb7b..4817e40 100644 --- a/docs/content/grafana_api/model.md +++ b/docs/content/grafana_api/model.md @@ -15,6 +15,8 @@ * [PlaylistObject](#grafana_api.model.PlaylistObject) * [PlaylistItemObject](#grafana_api.model.PlaylistItemObject) * [TeamObject](#grafana_api.model.TeamObject) + * [QueryDatasourceObject](#grafana_api.model.QueryDatasourceObject) + * [QueryObject](#grafana_api.model.QueryObject) @@ -259,3 +261,35 @@ The class includes all necessary variables to generate a team object that is nec - `email` _str_ - Specify the email of the team - `org_id` _int_ - Specify the org_id of the team + + +## QueryDatasourceObject Objects + +```python +class QueryDatasourceObject(NamedTuple) +``` + +The class includes all necessary variables to generate a query datasource object that is necessary to create a query history object + +**Arguments**: + +- `type` _str_ - Specify the type of the datasource query +- `uid` _str_ - Specify the uid of the datasource query + + + +## QueryObject Objects + +```python +class QueryObject(NamedTuple) +``` + +The class includes all necessary variables to generate a query object that is necessary to create a query history + +**Arguments**: + +- `ref_id` _str_ - Specify the ref_id of the query history +- `key` _str_ - Specify the key of the query history +- `scenario_id` _str_ - Specify the scenario_id of the query history +- `datasource` _QueryDatasourceObject_ - Specify the datasource of the type QueryDatasourceObject +