perses-api-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
perses_api/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ from .model import APIModel
2
+ from .api import Api
3
+ from .project import Project
4
+ from .dashboard import Dashboard
5
+ from .ephemeral_dashboard import EphemeralDashboard
6
+ from .datasource import ProjectDatasource, GlobalDatasource
7
+ from .variable import ProjectVariable, GlobalVariable
8
+ from .role import ProjectRole, GlobalRole
9
+ from .role_binding import ProjectRoleBinding, GlobalRoleBinding
10
+ from .secret import ProjectSecret, GlobalSecret
11
+ from .user import User
12
+ from .plugin import Plugin
13
+ from .migrate import Migrate
14
+ from .validate import Validate
15
+
16
+ __all__ = [
17
+ "APIModel",
18
+ "Api",
19
+ "Project",
20
+ "Dashboard",
21
+ "EphemeralDashboard",
22
+ "ProjectDatasource",
23
+ "GlobalDatasource",
24
+ "ProjectVariable",
25
+ "GlobalVariable",
26
+ "ProjectRole",
27
+ "GlobalRole",
28
+ "ProjectRoleBinding",
29
+ "GlobalRoleBinding",
30
+ "ProjectSecret",
31
+ "GlobalSecret",
32
+ "User",
33
+ "Plugin",
34
+ "Migrate",
35
+ "Validate",
36
+ ]
perses_api/_base.py ADDED
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from .api import Api
6
+ from .model import APIModel, RequestsMethods
7
+
8
+
9
+ class ResourceBase:
10
+ """The class includes all necessary base methods to access dual-scoped Perses resources
11
+
12
+ Args:
13
+ perses_api_model (APIModel): Inject a Perses API model object that includes all necessary values and information
14
+
15
+ Attributes:
16
+ api (Api): This is where we store the api
17
+ """
18
+
19
+ def __init__(self, perses_api_model: APIModel):
20
+ self.api = Api(perses_api_model)
21
+
22
+ def _base_path(self) -> str:
23
+ """The method returns the base API path for the resource type
24
+
25
+ Raises:
26
+ NotImplementedError: Subclasses must implement this method
27
+
28
+ Returns:
29
+ str: The base API path
30
+ """
31
+ raise NotImplementedError
32
+
33
+ def _get_all(self, name: str = None) -> list:
34
+ """The method includes a functionality to retrieve all resources, optionally filtered by name
35
+
36
+ Args:
37
+ name (str): Specify a name to filter the results (default None)
38
+
39
+ Raises:
40
+ Exception: Unspecified error by executing the API call
41
+
42
+ Returns:
43
+ list: A list of resource dicts
44
+ """
45
+ path = self._base_path()
46
+ if name:
47
+ path = f"{path}?name={name}"
48
+ result = self.api.call_the_api(path)
49
+ if not isinstance(result, list):
50
+ logging.error(f"Failed to retrieve resources from {self._base_path()}.")
51
+ raise Exception(result)
52
+ return result
53
+
54
+ def _get_one(self, name: str) -> dict:
55
+ """The method includes a functionality to retrieve a single resource by name
56
+
57
+ Args:
58
+ name (str): Specify the name of the resource to retrieve
59
+
60
+ Raises:
61
+ ValueError: Missed specifying a necessary value
62
+ Exception: Unspecified error by executing the API call
63
+
64
+ Returns:
65
+ dict: The resource dict
66
+ """
67
+ if not name:
68
+ raise ValueError("name must not be empty")
69
+ result = self.api.call_the_api(f"{self._base_path()}/{name}")
70
+ if not isinstance(result, dict):
71
+ logging.error(f"Failed to retrieve resource: {name}")
72
+ raise Exception(result)
73
+ return result
74
+
75
+ def _create(self, body) -> dict:
76
+ """The method includes a functionality to create a new resource
77
+
78
+ Args:
79
+ body (BaseModel): Specify the resource body as a Pydantic model instance
80
+
81
+ Raises:
82
+ Exception: Unspecified error by executing the API call
83
+
84
+ Returns:
85
+ dict: The created resource dict
86
+ """
87
+ result = self.api.call_the_api(
88
+ self._base_path(),
89
+ method=RequestsMethods.POST,
90
+ json_complete=body.model_dump_json(by_alias=True, exclude_none=True),
91
+ )
92
+ if not isinstance(result, dict):
93
+ logging.error(f"Failed to create resource at {self._base_path()}.")
94
+ raise Exception(result)
95
+ return result
96
+
97
+ def _update(self, name: str, body) -> dict:
98
+ """The method includes a functionality to update an existing resource by name
99
+
100
+ Args:
101
+ name (str): Specify the name of the resource to update
102
+ body (BaseModel): Specify the updated resource body as a Pydantic model instance
103
+
104
+ Raises:
105
+ ValueError: Missed specifying a necessary value
106
+ Exception: Unspecified error by executing the API call
107
+
108
+ Returns:
109
+ dict: The updated resource dict
110
+ """
111
+ if not name:
112
+ raise ValueError("name must not be empty")
113
+ result = self.api.call_the_api(
114
+ f"{self._base_path()}/{name}",
115
+ method=RequestsMethods.PUT,
116
+ json_complete=body.model_dump_json(by_alias=True, exclude_none=True),
117
+ )
118
+ if not isinstance(result, dict):
119
+ logging.error(f"Failed to update resource: {name}")
120
+ raise Exception(result)
121
+ return result
122
+
123
+ def _delete(self, name: str) -> None:
124
+ """The method includes a functionality to delete a resource by name
125
+
126
+ Args:
127
+ name (str): Specify the name of the resource to delete
128
+
129
+ Raises:
130
+ ValueError: Missed specifying a necessary value
131
+ """
132
+ if not name:
133
+ raise ValueError("name must not be empty")
134
+ self.api.call_the_api(
135
+ f"{self._base_path()}/{name}", method=RequestsMethods.DELETE
136
+ )
perses_api/api.py ADDED
@@ -0,0 +1,190 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import base64
5
+ import json
6
+ import logging
7
+ from typing import Any, Union
8
+
9
+ import httpx
10
+
11
+ from .model import APIModel, RequestsMethods
12
+
13
+
14
+ class Api:
15
+ """The class includes all necessary methods to access the Perses API
16
+
17
+ Args:
18
+ perses_api_model (APIModel): Inject a Perses API model object that includes all necessary values and information
19
+
20
+ Attributes:
21
+ perses_api_model (APIModel): This is where we store the perses_api_model
22
+ """
23
+
24
+ def __init__(self, perses_api_model: APIModel):
25
+ self.perses_api_model = perses_api_model
26
+
27
+ def call_the_api(
28
+ self,
29
+ api_call: str,
30
+ method: RequestsMethods = RequestsMethods.GET,
31
+ json_complete: str = None,
32
+ response_status_code: bool = False,
33
+ ) -> Any:
34
+ """The method includes a functionality to execute a defined API call against the Perses endpoints
35
+
36
+ Args:
37
+ api_call (str): Specify the API call path relative to the host
38
+ method (RequestsMethods): Specify the HTTP method to use (default GET)
39
+ json_complete (str): Specify the JSON-serialised request body for POST and PUT requests (default None)
40
+ response_status_code (bool): Specify if the HTTP status code should be injected into the response dict (default False)
41
+
42
+ Raises:
43
+ ValueError: Missed specifying a necessary value
44
+ Exception: Unspecified error by executing the API call
45
+
46
+ Returns:
47
+ any: The API response as a parsed dict or list, or the raw httpx.Response for non-JSON responses
48
+ """
49
+ api_url = f"{self.perses_api_model.host}{api_call}"
50
+ headers = dict(self.perses_api_model.headers or {})
51
+
52
+ if self.perses_api_model.token:
53
+ headers["Authorization"] = f"Bearer {self.perses_api_model.token}"
54
+ elif self.perses_api_model.username and self.perses_api_model.password:
55
+ credentials = base64.b64encode(
56
+ f"{self.perses_api_model.username}:{self.perses_api_model.password}".encode()
57
+ ).decode("utf-8")
58
+ headers["Authorization"] = f"Basic {credentials}"
59
+
60
+ headers["Content-Type"] = "application/json"
61
+ headers["Accept"] = "application/json"
62
+
63
+ http = self.create_the_http_api_client(headers)
64
+
65
+ if self.perses_api_model.http2_support:
66
+
67
+ async def _run():
68
+ async with http:
69
+ return self._check_the_api_call_response(
70
+ await self._send_request(http, method, api_url, json_complete),
71
+ response_status_code,
72
+ )
73
+
74
+ return asyncio.run(_run())
75
+
76
+ response = self._send_request(http, method, api_url, json_complete)
77
+ return self._check_the_api_call_response(response, response_status_code)
78
+
79
+ def create_the_http_api_client(
80
+ self, headers: dict = None
81
+ ) -> Union[httpx.Client, httpx.AsyncClient]:
82
+ """The method includes a functionality to create the HTTP client based on the API model configuration
83
+
84
+ Args:
85
+ headers (dict): Specify the HTTP headers to attach to every request (default None)
86
+
87
+ Returns:
88
+ Union[httpx.Client, httpx.AsyncClient]: A configured sync or async httpx client
89
+ """
90
+ limits = httpx.Limits(max_connections=self.perses_api_model.num_pools)
91
+
92
+ if self.perses_api_model.http2_support:
93
+ transport = httpx.AsyncHTTPTransport(
94
+ retries=self.perses_api_model.retries,
95
+ http2=True,
96
+ )
97
+ return httpx.AsyncClient(
98
+ http2=True,
99
+ limits=limits,
100
+ timeout=self.perses_api_model.timeout,
101
+ headers=headers,
102
+ transport=transport,
103
+ verify=self.perses_api_model.ssl_context or True,
104
+ follow_redirects=self.perses_api_model.follow_redirects,
105
+ )
106
+
107
+ transport = httpx.HTTPTransport(
108
+ verify=self.perses_api_model.ssl_context or True,
109
+ retries=self.perses_api_model.retries,
110
+ )
111
+ return httpx.Client(
112
+ limits=limits,
113
+ timeout=self.perses_api_model.timeout,
114
+ headers=headers,
115
+ transport=transport,
116
+ verify=self.perses_api_model.ssl_context or True,
117
+ follow_redirects=self.perses_api_model.follow_redirects,
118
+ )
119
+
120
+ def _send_request(
121
+ self,
122
+ http: Union[httpx.Client, httpx.AsyncClient],
123
+ method: RequestsMethods,
124
+ api_url: str,
125
+ json_complete: str,
126
+ ) -> Any:
127
+ """The method includes a functionality to dispatch a single HTTP request using the provided client
128
+
129
+ Args:
130
+ http (Union[httpx.Client, httpx.AsyncClient]): Specify the httpx client to use for the request
131
+ method (RequestsMethods): Specify the HTTP method
132
+ api_url (str): Specify the fully-qualified request URL
133
+ json_complete (str): Specify the JSON-serialised request body for POST and PUT (default None)
134
+
135
+ Raises:
136
+ ValueError: Missed specifying a necessary value for POST or PUT requests
137
+
138
+ Returns:
139
+ any: The raw httpx response object
140
+ """
141
+ if method in (RequestsMethods.GET, RequestsMethods.DELETE):
142
+ return http.request(method.value, api_url)
143
+ if json_complete is None:
144
+ logging.error("Please define the json_complete.")
145
+ raise ValueError(f"json_complete is required for {method.value}")
146
+ return http.request(method.value, api_url, content=json_complete)
147
+
148
+ @staticmethod
149
+ def _check_the_api_call_response(
150
+ response: Any, response_status_code: bool = False
151
+ ) -> Any:
152
+ """The method includes a functionality to parse the API response and optionally inject the HTTP status code
153
+
154
+ Args:
155
+ response (any): Specify the raw httpx response object
156
+ response_status_code (bool): Specify if the HTTP status code should be injected into the response (default False)
157
+
158
+ Returns:
159
+ any: The parsed JSON response as dict or list, or the raw response for non-JSON bodies
160
+ """
161
+ if Api._check_if_valid_json(response.text):
162
+ json_response = json.loads(response.text)
163
+ if response_status_code:
164
+ if isinstance(json_response, dict):
165
+ json_response["status"] = response.status_code
166
+ elif isinstance(json_response, list) and json_response:
167
+ json_response[0]["status"] = response.status_code
168
+ return json_response
169
+ else:
170
+ if response_status_code:
171
+ return {"status": response.status_code, "data": response.text}
172
+ return response
173
+
174
+ @staticmethod
175
+ def _check_if_valid_json(response: str) -> bool:
176
+ """The method includes a functionality to check if the given string is valid JSON
177
+
178
+ Args:
179
+ response (str): Specify the string to validate
180
+
181
+ Returns:
182
+ bool: True if the string is valid JSON, False otherwise
183
+ """
184
+ if not response or response.strip() in ("", "null"):
185
+ return False
186
+ try:
187
+ json.loads(response)
188
+ return True
189
+ except (TypeError, ValueError):
190
+ return False
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from .api import Api
6
+ from .model import APIModel, APIEndpoints, RequestsMethods
7
+ from .model import Dashboard as DashboardModel
8
+
9
+
10
+ class Dashboard:
11
+ """The class includes all necessary methods to access the Perses dashboards API
12
+
13
+ Args:
14
+ perses_api_model (APIModel): Inject a Perses API model object that includes all necessary values and information
15
+
16
+ Attributes:
17
+ api (Api): This is where we store the api
18
+ """
19
+
20
+ def __init__(self, perses_api_model: APIModel):
21
+ self.api = Api(perses_api_model)
22
+
23
+ def get_dashboards(self, project_name: str, name: str = None) -> list:
24
+ """The method includes a functionality to retrieve all dashboards within a project
25
+
26
+ Args:
27
+ project_name (str): Specify the name of the project to list dashboards for
28
+ name (str): Specify a name to filter the results (default None)
29
+
30
+ Raises:
31
+ ValueError: Missed specifying a necessary value
32
+ Exception: Unspecified error by executing the API call
33
+
34
+ Returns:
35
+ list: A list of dashboard dicts
36
+ """
37
+ if not project_name:
38
+ raise ValueError("project_name must not be empty")
39
+ path = APIEndpoints.DASHBOARDS.value.format(project=project_name)
40
+ if name:
41
+ path = f"{path}?name={name}"
42
+ result = self.api.call_the_api(path)
43
+ if not isinstance(result, list):
44
+ logging.error("Failed to retrieve dashboards.")
45
+ raise Exception(result)
46
+ return result
47
+
48
+ def get_dashboard(self, project_name: str, name: str) -> dict:
49
+ """The method includes a functionality to retrieve a single dashboard by name
50
+
51
+ Args:
52
+ project_name (str): Specify the name of the project the dashboard belongs to
53
+ name (str): Specify the name of the dashboard to retrieve
54
+
55
+ Raises:
56
+ ValueError: Missed specifying a necessary value
57
+ Exception: Unspecified error by executing the API call
58
+
59
+ Returns:
60
+ dict: The dashboard dict
61
+ """
62
+ if not project_name:
63
+ raise ValueError("project_name must not be empty")
64
+ if not name:
65
+ raise ValueError("name must not be empty")
66
+ result = self.api.call_the_api(
67
+ APIEndpoints.DASHBOARD.value.format(project=project_name, name=name)
68
+ )
69
+ if not isinstance(result, dict):
70
+ logging.error(f"Failed to retrieve dashboard: {name}")
71
+ raise Exception(result)
72
+ return result
73
+
74
+ def create_dashboard(self, project_name: str, dashboard: DashboardModel) -> dict:
75
+ """The method includes a functionality to create a new dashboard within a project
76
+
77
+ Args:
78
+ project_name (str): Specify the name of the project to create the dashboard in
79
+ dashboard (Dashboard): Specify the dashboard resource to create
80
+
81
+ Raises:
82
+ ValueError: Missed specifying a necessary value
83
+ Exception: Unspecified error by executing the API call
84
+
85
+ Returns:
86
+ dict: The created dashboard dict
87
+ """
88
+ if not project_name:
89
+ raise ValueError("project_name must not be empty")
90
+ result = self.api.call_the_api(
91
+ APIEndpoints.DASHBOARDS.value.format(project=project_name),
92
+ method=RequestsMethods.POST,
93
+ json_complete=dashboard.model_dump_json(by_alias=True, exclude_none=True),
94
+ )
95
+ if not isinstance(result, dict):
96
+ logging.error("Failed to create dashboard.")
97
+ raise Exception(result)
98
+ return result
99
+
100
+ def update_dashboard(
101
+ self, project_name: str, name: str, dashboard: DashboardModel
102
+ ) -> dict:
103
+ """The method includes a functionality to update an existing dashboard by name
104
+
105
+ Args:
106
+ project_name (str): Specify the name of the project the dashboard belongs to
107
+ name (str): Specify the name of the dashboard to update
108
+ dashboard (Dashboard): Specify the updated dashboard resource
109
+
110
+ Raises:
111
+ ValueError: Missed specifying a necessary value
112
+ Exception: Unspecified error by executing the API call
113
+
114
+ Returns:
115
+ dict: The updated dashboard dict
116
+ """
117
+ if not project_name:
118
+ raise ValueError("project_name must not be empty")
119
+ if not name:
120
+ raise ValueError("name must not be empty")
121
+ result = self.api.call_the_api(
122
+ APIEndpoints.DASHBOARD.value.format(project=project_name, name=name),
123
+ method=RequestsMethods.PUT,
124
+ json_complete=dashboard.model_dump_json(by_alias=True, exclude_none=True),
125
+ )
126
+ if not isinstance(result, dict):
127
+ logging.error(f"Failed to update dashboard: {name}")
128
+ raise Exception(result)
129
+ return result
130
+
131
+ def delete_dashboard(self, project_name: str, name: str) -> None:
132
+ """The method includes a functionality to delete a dashboard by name
133
+
134
+ Args:
135
+ project_name (str): Specify the name of the project the dashboard belongs to
136
+ name (str): Specify the name of the dashboard to delete
137
+
138
+ Raises:
139
+ ValueError: Missed specifying a necessary value
140
+ """
141
+ if not project_name:
142
+ raise ValueError("project_name must not be empty")
143
+ if not name:
144
+ raise ValueError("name must not be empty")
145
+ self.api.call_the_api(
146
+ APIEndpoints.DASHBOARD.value.format(project=project_name, name=name),
147
+ method=RequestsMethods.DELETE,
148
+ )
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ from ._base import ResourceBase
4
+ from .model import APIModel, APIEndpoints
5
+
6
+
7
+ class DatasourceBase(ResourceBase):
8
+ """The class includes all necessary base methods to access the Perses datasources API
9
+
10
+ Args:
11
+ perses_api_model (APIModel): Inject a Perses API model object that includes all necessary values and information
12
+
13
+ Attributes:
14
+ api (Api): This is where we store the api
15
+ """
16
+
17
+ def get_datasources(self, name: str = None) -> list:
18
+ """The method includes a functionality to retrieve all datasources
19
+
20
+ Args:
21
+ name (str): Specify a name to filter the results (default None)
22
+
23
+ Raises:
24
+ Exception: Unspecified error by executing the API call
25
+
26
+ Returns:
27
+ list: A list of datasource dicts
28
+ """
29
+ return self._get_all(name)
30
+
31
+ def get_datasource(self, name: str) -> dict:
32
+ """The method includes a functionality to retrieve a single datasource by name
33
+
34
+ Args:
35
+ name (str): Specify the name of the datasource to retrieve
36
+
37
+ Raises:
38
+ ValueError: Missed specifying a necessary value
39
+ Exception: Unspecified error by executing the API call
40
+
41
+ Returns:
42
+ dict: The datasource dict
43
+ """
44
+ return self._get_one(name)
45
+
46
+ def create_datasource(self, datasource) -> dict:
47
+ """The method includes a functionality to create a new datasource
48
+
49
+ Args:
50
+ datasource (Datasource | GlobalDatasource): Specify the datasource resource to create
51
+
52
+ Raises:
53
+ Exception: Unspecified error by executing the API call
54
+
55
+ Returns:
56
+ dict: The created datasource dict
57
+ """
58
+ return self._create(datasource)
59
+
60
+ def update_datasource(self, name: str, datasource) -> dict:
61
+ """The method includes a functionality to update an existing datasource by name
62
+
63
+ Args:
64
+ name (str): Specify the name of the datasource to update
65
+ datasource (Datasource | GlobalDatasource): Specify the updated datasource resource
66
+
67
+ Raises:
68
+ ValueError: Missed specifying a necessary value
69
+ Exception: Unspecified error by executing the API call
70
+
71
+ Returns:
72
+ dict: The updated datasource dict
73
+ """
74
+ return self._update(name, datasource)
75
+
76
+ def delete_datasource(self, name: str) -> None:
77
+ """The method includes a functionality to delete a datasource by name
78
+
79
+ Args:
80
+ name (str): Specify the name of the datasource to delete
81
+
82
+ Raises:
83
+ ValueError: Missed specifying a necessary value
84
+ """
85
+ self._delete(name)
86
+
87
+
88
+ class ProjectDatasource(DatasourceBase):
89
+ """The class includes all necessary methods to access the Perses project-scoped datasources API
90
+
91
+ Args:
92
+ perses_api_model (APIModel): Inject a Perses API model object that includes all necessary values and information
93
+ project_name (str): Specify the name of the project to scope datasource operations to
94
+
95
+ Attributes:
96
+ api (Api): This is where we store the api
97
+ project_name (str): This is where we store the project_name
98
+ """
99
+
100
+ def __init__(self, perses_api_model: APIModel, project_name: str):
101
+ super().__init__(perses_api_model)
102
+ self.project_name = project_name
103
+
104
+ def _base_path(self) -> str:
105
+ return APIEndpoints.DATASOURCES.value.format(project=self.project_name)
106
+
107
+
108
+ class GlobalDatasource(DatasourceBase):
109
+ """The class includes all necessary methods to access the Perses global datasources API
110
+
111
+ Args:
112
+ perses_api_model (APIModel): Inject a Perses API model object that includes all necessary values and information
113
+
114
+ Attributes:
115
+ api (Api): This is where we store the api
116
+ """
117
+
118
+ def _base_path(self) -> str:
119
+ return APIEndpoints.GLOBAL_DATASOURCES.value