Skip to content

Commit a25c4ac

Browse files
authored
Add cw 22 support (#11)
Add query history, licensing, other http, reporting support and documentation
1 parent ae90099 commit a25c4ac

File tree

16 files changed

+1266
-40
lines changed

16 files changed

+1266
-40
lines changed

docs/content/grafana_api/api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ The class includes all necessary methods to make API calls to the Grafana API en
3434
```python
3535
def call_the_api(api_call: str,
3636
method: RequestsMethods = RequestsMethods.GET,
37-
json_complete: str = None) -> any
37+
json_complete: str = None,
38+
timeout: float = None) -> any
3839
```
3940

4041
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
4445
- `api_call` _str_ - Specify the API call endpoint
4546
- `method` _RequestsMethods_ - Specify the used method (default GET)
4647
- `json_complete` _str_ - Specify the inserted JSON as string
48+
- `timeout` _float_ - Specify the timeout for the corresponding API call
4749

4850

4951
**Raises**:

docs/content/grafana_api/model.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
* [PlaylistObject](#grafana_api.model.PlaylistObject)
1616
* [PlaylistItemObject](#grafana_api.model.PlaylistItemObject)
1717
* [TeamObject](#grafana_api.model.TeamObject)
18+
* [QueryDatasourceObject](#grafana_api.model.QueryDatasourceObject)
19+
* [QueryObject](#grafana_api.model.QueryObject)
1820

1921
<a id="grafana_api.model"></a>
2022

@@ -259,3 +261,35 @@ The class includes all necessary variables to generate a team object that is nec
259261
- `email` _str_ - Specify the email of the team
260262
- `org_id` _int_ - Specify the org_id of the team
261263

264+
<a id="grafana_api.model.QueryDatasourceObject"></a>
265+
266+
## QueryDatasourceObject Objects
267+
268+
```python
269+
class QueryDatasourceObject(NamedTuple)
270+
```
271+
272+
The class includes all necessary variables to generate a query datasource object that is necessary to create a query history object
273+
274+
**Arguments**:
275+
276+
- `type` _str_ - Specify the type of the datasource query
277+
- `uid` _str_ - Specify the uid of the datasource query
278+
279+
<a id="grafana_api.model.QueryObject"></a>
280+
281+
## QueryObject Objects
282+
283+
```python
284+
class QueryObject(NamedTuple)
285+
```
286+
287+
The class includes all necessary variables to generate a query object that is necessary to create a query history
288+
289+
**Arguments**:
290+
291+
- `ref_id` _str_ - Specify the ref_id of the query history
292+
- `key` _str_ - Specify the key of the query history
293+
- `scenario_id` _str_ - Specify the scenario_id of the query history
294+
- `datasource` _QueryDatasourceObject_ - Specify the datasource of the type QueryDatasourceObject
295+

src/grafana_api/api.py

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import json
23

34
import requests
45

@@ -23,13 +24,15 @@ def call_the_api(
2324
api_call: str,
2425
method: RequestsMethods = RequestsMethods.GET,
2526
json_complete: str = None,
27+
timeout: float = None,
2628
) -> any:
2729
"""The method execute a defined API call against the Grafana endpoints
2830
2931
Args:
3032
api_call (str): Specify the API call endpoint
3133
method (RequestsMethods): Specify the used method (default GET)
3234
json_complete (str): Specify the inserted JSON as string
35+
timeout (float): Specify the timeout for the corresponding API call
3336
3437
Raises:
3538
Exception: Unspecified error by executing the API call
@@ -58,35 +61,50 @@ def call_the_api(
5861
try:
5962
if method.value == RequestsMethods.GET.value:
6063
return Api.__check_the_api_call_response(
61-
requests.get(api_url, headers=headers)
64+
requests.get(api_url, headers=headers, timeout=timeout)
6265
)
6366
elif method.value == RequestsMethods.PUT.value:
6467
if json_complete is not None:
6568
return Api.__check_the_api_call_response(
66-
requests.put(api_url, data=json_complete, headers=headers)
69+
requests.put(
70+
api_url,
71+
data=json_complete,
72+
headers=headers,
73+
timeout=timeout,
74+
)
6775
)
6876
else:
6977
logging.error("Please define the json_complete.")
7078
raise Exception
7179
elif method.value == RequestsMethods.POST.value:
7280
if json_complete is not None:
7381
return Api.__check_the_api_call_response(
74-
requests.post(api_url, data=json_complete, headers=headers)
82+
requests.post(
83+
api_url,
84+
data=json_complete,
85+
headers=headers,
86+
timeout=timeout,
87+
)
7588
)
7689
else:
7790
logging.error("Please define the json_complete.")
7891
raise Exception
7992
elif method.value == RequestsMethods.PATCH.value:
8093
if json_complete is not None:
8194
return Api.__check_the_api_call_response(
82-
requests.patch(api_url, data=json_complete, headers=headers)
95+
requests.patch(
96+
api_url,
97+
data=json_complete,
98+
headers=headers,
99+
timeout=timeout,
100+
)
83101
)
84102
else:
85103
logging.error("Please define the json_complete.")
86104
raise Exception
87105
elif method.value == RequestsMethods.DELETE.value:
88106
return Api.__check_the_api_call_response(
89-
requests.delete(api_url, headers=headers)
107+
requests.delete(api_url, headers=headers, timeout=timeout)
90108
)
91109
else:
92110
logging.error("Please define a valid method.")
@@ -108,12 +126,30 @@ def __check_the_api_call_response(response: any = None) -> any:
108126
api_call (any): Returns the value of the api call
109127
"""
110128

111-
if len(response.text) != 0 and type(response.json()) == dict:
112-
if (
113-
"message" in response.json().keys()
114-
and response.json()["message"] in ERROR_MESSAGES
115-
):
116-
logging.error(response.json()["message"])
117-
raise requests.exceptions.ConnectionError
129+
if Api.__check_if_valid_json(response.text):
130+
if len(response.text) != 0 and type(response.json()) == dict:
131+
if (
132+
"message" in response.json().keys()
133+
and response.json()["message"] in ERROR_MESSAGES
134+
):
135+
logging.error(response.json()["message"])
136+
raise requests.exceptions.ConnectionError
118137

119138
return response
139+
140+
@staticmethod
141+
def __check_if_valid_json(response: any) -> bool:
142+
"""The method includes a functionality to check if the response json is valid
143+
144+
Args:
145+
response (any): Specify the inserted response json
146+
147+
Returns:
148+
api_call (bool): Returns if the json is valid or not
149+
"""
150+
151+
try:
152+
json.loads(response)
153+
except ValueError:
154+
return False
155+
return True

src/grafana_api/licensing.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import json
2+
from requests import Response
3+
import logging
4+
5+
from .model import (
6+
APIModel,
7+
APIEndpoints,
8+
RequestsMethods,
9+
)
10+
from .api import Api
11+
12+
13+
class Licensing:
14+
"""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
15+
16+
HINT: Note Grafana Enterprise API need required permissions if fine-grained access control is enabled
17+
18+
Args:
19+
grafana_api_model (APIModel): Inject a Grafana API model object that includes all necessary values and information
20+
21+
Attributes:
22+
grafana_api_model (APIModel): This is where we store the grafana_api_model
23+
"""
24+
25+
def __init__(self, grafana_api_model: APIModel):
26+
self.grafana_api_model = grafana_api_model
27+
28+
def check_license_availability(self):
29+
"""The method includes a functionality to checks if a valid license is available
30+
31+
Required Permissions:
32+
Action: licensing:read
33+
Scope: N/A
34+
35+
Raises:
36+
Exception: Unspecified error by executing the API call
37+
38+
Returns:
39+
api_call (bool): Returns the result if the license is available or not
40+
"""
41+
42+
api_call: Response = Api(self.grafana_api_model).call_the_api(
43+
f"{APIEndpoints.LICENSING.value}/check",
44+
)
45+
46+
if api_call.status_code != 200:
47+
logging.error(f"Check the error: {api_call}.")
48+
raise Exception
49+
else:
50+
return json.loads(str(api_call.text))
51+
52+
def manually_force_license_refresh(self):
53+
"""The method includes a functionality to manually ask license issuer for a new token
54+
55+
Required Permissions:
56+
Action: licensing:update
57+
Scope: N/A
58+
59+
Raises:
60+
Exception: Unspecified error by executing the API call
61+
62+
Returns:
63+
api_call (dict): Returns the result of license refresh call
64+
"""
65+
66+
api_call: dict = (
67+
Api(self.grafana_api_model)
68+
.call_the_api(
69+
f"{APIEndpoints.LICENSING.value}/token/renew",
70+
RequestsMethods.POST,
71+
json.dumps({}),
72+
)
73+
.json()
74+
)
75+
76+
if api_call == dict() or api_call.get("jti") is None:
77+
logging.error(f"Check the error: {api_call}.")
78+
raise Exception
79+
else:
80+
return api_call
81+
82+
def remove_license_from_dashboard(self):
83+
"""The method includes a functionality to removes the license stored in the Grafana database
84+
85+
Required Permissions:
86+
Action: licensing:delete
87+
Scope: N/A
88+
89+
Raises:
90+
Exception: Unspecified error by executing the API call
91+
92+
Returns:
93+
api_call (dict): Returns the result of license refresh call
94+
"""
95+
96+
api_call: Response = Api(self.grafana_api_model).call_the_api(
97+
f"{APIEndpoints.LICENSING.value}/token",
98+
RequestsMethods.DELETE,
99+
)
100+
101+
if api_call.status_code != 200:
102+
logging.error(f"Check the error: {api_call}.")
103+
raise Exception
104+
else:
105+
logging.info(
106+
"You successfully removed the corresponding license from the database."
107+
)

src/grafana_api/model.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ class APIEndpoints(Enum):
3131
DASHBOARD_SNAPSHOTS = f"{api_prefix}/dashboard/snapshots"
3232
PLAYLISTS = f"{api_prefix}/playlists"
3333
TEAMS = f"{api_prefix}/teams"
34+
QUERY_HISTORY = f"{api_prefix}/query-history"
35+
REPORTING = f"{api_prefix}/reports/email"
36+
LICENSING = f"{api_prefix}/licensing"
37+
FRONTEND = f"{api_prefix}/frontend"
38+
LOGIN = f"{api_prefix}/login"
3439

3540

3641
class RequestsMethods(Enum):
@@ -264,3 +269,31 @@ class TeamObject(NamedTuple):
264269
name: str
265270
email: str
266271
org_id: int
272+
273+
274+
class QueryDatasourceObject(NamedTuple):
275+
"""The class includes all necessary variables to generate a query datasource object that is necessary to create a query history object
276+
277+
Args:
278+
type (str): Specify the type of the datasource query
279+
uid (str): Specify the uid of the datasource query
280+
"""
281+
282+
type: str
283+
uid: str
284+
285+
286+
class QueryObject(NamedTuple):
287+
"""The class includes all necessary variables to generate a query object that is necessary to create a query history
288+
289+
Args:
290+
ref_id (str): Specify the ref_id of the query history
291+
key (str): Specify the key of the query history
292+
scenario_id (str): Specify the scenario_id of the query history
293+
datasource (QueryDatasourceObject): Specify the datasource of the type QueryDatasourceObject
294+
"""
295+
296+
ref_id: str
297+
key: str
298+
scenario_id: str
299+
datasource: QueryDatasourceObject

0 commit comments

Comments
 (0)