openrelik-api-client 0.3.0__tar.gz → 0.5.0__tar.gz
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.
- {openrelik_api_client-0.3.0 → openrelik_api_client-0.5.0}/PKG-INFO +1 -1
- openrelik_api_client-0.5.0/openrelik_api_client/api_client.py +174 -0
- openrelik_api_client-0.5.0/openrelik_api_client/configs.py +31 -0
- openrelik_api_client-0.3.0/openrelik_api_client/api_client.py → openrelik_api_client-0.5.0/openrelik_api_client/files.py +86 -109
- {openrelik_api_client-0.3.0 → openrelik_api_client-0.5.0}/openrelik_api_client/folders.py +43 -5
- {openrelik_api_client-0.3.0 → openrelik_api_client-0.5.0}/openrelik_api_client/workflows.py +21 -2
- {openrelik_api_client-0.3.0 → openrelik_api_client-0.5.0}/pyproject.toml +12 -1
- {openrelik_api_client-0.3.0 → openrelik_api_client-0.5.0}/LICENSE +0 -0
- {openrelik_api_client-0.3.0 → openrelik_api_client-0.5.0}/README.md +0 -0
- {openrelik_api_client-0.3.0 → openrelik_api_client-0.5.0}/openrelik_api_client/__init__.py +0 -0
- {openrelik_api_client-0.3.0 → openrelik_api_client-0.5.0}/openrelik_api_client/groups.py +0 -0
@@ -0,0 +1,174 @@
|
|
1
|
+
# Copyright 2024 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
import warnings
|
16
|
+
from typing import Any
|
17
|
+
|
18
|
+
import requests
|
19
|
+
from requests.exceptions import RequestException
|
20
|
+
|
21
|
+
from .configs import ConfigsAPI
|
22
|
+
from .files import FilesAPI
|
23
|
+
|
24
|
+
|
25
|
+
class APIClient:
|
26
|
+
"""
|
27
|
+
A reusable API client that automatically handles token refresh on 401 responses.
|
28
|
+
|
29
|
+
Attributes:
|
30
|
+
api_server_url (str): The URL of the API server.
|
31
|
+
api_key (str): The API key.
|
32
|
+
api_version (str): The API version.
|
33
|
+
session (requests.Session): The session object.
|
34
|
+
|
35
|
+
Example usage:
|
36
|
+
client = APIClient(api_server_url, refresh_token)
|
37
|
+
response = client.get("/users/me/")
|
38
|
+
print(response.json())
|
39
|
+
"""
|
40
|
+
|
41
|
+
def __init__(
|
42
|
+
self,
|
43
|
+
api_server_url,
|
44
|
+
api_key=None,
|
45
|
+
api_version="v1",
|
46
|
+
):
|
47
|
+
self.base_url = f"{api_server_url}/api/{api_version}"
|
48
|
+
self.session = TokenRefreshSession(api_server_url, api_key)
|
49
|
+
|
50
|
+
def get(self, endpoint, **kwargs):
|
51
|
+
"""Sends a GET request to the specified API endpoint."""
|
52
|
+
url = f"{self.base_url}{endpoint}"
|
53
|
+
return self.session.get(url, **kwargs)
|
54
|
+
|
55
|
+
def post(self, endpoint, data=None, json=None, **kwargs):
|
56
|
+
"""Sends a POST request to the specified API endpoint."""
|
57
|
+
url = f"{self.base_url}{endpoint}"
|
58
|
+
return self.session.post(url, data=data, json=json, **kwargs)
|
59
|
+
|
60
|
+
def put(self, endpoint, data=None, **kwargs):
|
61
|
+
"""Sends a PUT request to the specified API endpoint."""
|
62
|
+
url = f"{self.base_url}{endpoint}"
|
63
|
+
return self.session.put(url, data=data, **kwargs)
|
64
|
+
|
65
|
+
def patch(self, endpoint, data=None, json=None, **kwargs):
|
66
|
+
"""Sends a PATCH request to the specified API endpoint."""
|
67
|
+
url = f"{self.base_url}{endpoint}"
|
68
|
+
return self.session.patch(url, data=data, json=json, **kwargs)
|
69
|
+
|
70
|
+
def delete(self, endpoint, **kwargs):
|
71
|
+
"""Sends a DELETE request to the specified API endpoint."""
|
72
|
+
url = f"{self.base_url}{endpoint}"
|
73
|
+
return self.session.delete(url, **kwargs)
|
74
|
+
|
75
|
+
def get_config(self) -> dict[str, Any]:
|
76
|
+
"""
|
77
|
+
DEPRECATED: Use configAPI.get_system_config() instead.
|
78
|
+
This method will be removed in a future version.
|
79
|
+
"""
|
80
|
+
warnings.warn(
|
81
|
+
"The 'APIClient.get_config' method is deprecated. Please use 'configAPI.get_system_config()' instead.",
|
82
|
+
DeprecationWarning,
|
83
|
+
stacklevel=2,
|
84
|
+
)
|
85
|
+
configs_api = ConfigsAPI(self)
|
86
|
+
return configs_api.get_system_config()
|
87
|
+
|
88
|
+
def download_file(self, file_id: int, filename: str) -> str | None:
|
89
|
+
"""
|
90
|
+
DEPRECATED: Use filesAPI.download_file() instead.
|
91
|
+
This method will be removed in a future version.
|
92
|
+
"""
|
93
|
+
warnings.warn(
|
94
|
+
"The 'APIClient.download_file' method is deprecated. Please use 'filesAPI.download_file()' instead.",
|
95
|
+
DeprecationWarning,
|
96
|
+
stacklevel=2,
|
97
|
+
)
|
98
|
+
files_api = FilesAPI(self)
|
99
|
+
return files_api.download_file(file_id, filename)
|
100
|
+
|
101
|
+
def upload_file(self, file_path: str, folder_id: int) -> int | None:
|
102
|
+
"""
|
103
|
+
DEPRECATED: Use filesAPI.download_file() instead.
|
104
|
+
This method will be removed in a future version.
|
105
|
+
"""
|
106
|
+
warnings.warn(
|
107
|
+
"The 'APIClient.upload_file' method is deprecated. Please use 'filesAPI.upload_file()' instead.",
|
108
|
+
DeprecationWarning,
|
109
|
+
stacklevel=2,
|
110
|
+
)
|
111
|
+
files_api = FilesAPI(self)
|
112
|
+
return files_api.upload_file(file_path, folder_id)
|
113
|
+
|
114
|
+
|
115
|
+
class TokenRefreshSession(requests.Session):
|
116
|
+
"""Custom session class that handles automatic token refresh on 401 responses."""
|
117
|
+
|
118
|
+
def __init__(self, api_server_url, api_key):
|
119
|
+
"""
|
120
|
+
Initializes the TokenRefreshSession with the API server URL and refresh token.
|
121
|
+
|
122
|
+
Args:
|
123
|
+
api_server_url (str): The URL of the API server.
|
124
|
+
refresh_token (str): The refresh token.
|
125
|
+
"""
|
126
|
+
super().__init__()
|
127
|
+
self.api_server_url = api_server_url
|
128
|
+
if api_key:
|
129
|
+
self.headers["x-openrelik-refresh-token"] = api_key
|
130
|
+
|
131
|
+
def request(self, method: str, url: str, **kwargs: dict[str, Any]) -> requests.Response:
|
132
|
+
"""Intercepts the request to handle token expiration.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
method (str): The HTTP method.
|
136
|
+
url (str): The URL of the request.
|
137
|
+
**kwargs: Additional keyword arguments for the request.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
Response: The response object.
|
141
|
+
|
142
|
+
Raises:
|
143
|
+
Exception: If the token refresh fails.
|
144
|
+
"""
|
145
|
+
response = super().request(method, url, **kwargs)
|
146
|
+
|
147
|
+
if response.status_code == 401:
|
148
|
+
if self._refresh_token(url):
|
149
|
+
# Retry the original request with the new token
|
150
|
+
response = super().request(method, url, **kwargs)
|
151
|
+
else:
|
152
|
+
raise RuntimeError("API key has expired")
|
153
|
+
|
154
|
+
return response
|
155
|
+
|
156
|
+
def _refresh_token(self, requested_url: str) -> bool:
|
157
|
+
"""Refreshes the access token using the refresh token."""
|
158
|
+
refresh_url = f"{self.api_server_url}/auth/refresh"
|
159
|
+
|
160
|
+
# If the original URL is the same as the refresh URL, do not attempt to refresh as this
|
161
|
+
# indicates a faulty or expired api key. This prevents an infinite loop of refresh attempts.
|
162
|
+
if requested_url == refresh_url:
|
163
|
+
return False
|
164
|
+
|
165
|
+
try:
|
166
|
+
response = self.get(refresh_url)
|
167
|
+
response.raise_for_status()
|
168
|
+
# Update session headers with the new access token
|
169
|
+
new_access_token = response.json().get("new_access_token")
|
170
|
+
self.headers["x-openrelik-access-token"] = new_access_token
|
171
|
+
return True
|
172
|
+
except RequestException as e:
|
173
|
+
print(f"Failed to refresh token: {e}")
|
174
|
+
return False
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
from typing import TYPE_CHECKING, Any
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from openrelik_api_client.api_client import APIClient
|
19
|
+
|
20
|
+
|
21
|
+
class ConfigsAPI:
|
22
|
+
def __init__(self, api_client: "APIClient"):
|
23
|
+
super().__init__()
|
24
|
+
self.api_client = api_client
|
25
|
+
|
26
|
+
def get_system_config(self) -> dict[str, Any]:
|
27
|
+
"""Gets the current OpenRelik system configuration."""
|
28
|
+
endpoint = f"{self.api_client.base_url}/configs/system/"
|
29
|
+
response = self.api_client.session.get(endpoint)
|
30
|
+
response.raise_for_status()
|
31
|
+
return response.json()
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright
|
1
|
+
# Copyright 2025 Google LLC
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -17,71 +17,73 @@ import os
|
|
17
17
|
import tempfile
|
18
18
|
import time
|
19
19
|
from pathlib import Path
|
20
|
-
from typing import Any
|
20
|
+
from typing import TYPE_CHECKING, Any
|
21
21
|
from uuid import uuid4
|
22
22
|
|
23
|
-
import requests
|
24
|
-
from requests.exceptions import RequestException
|
25
23
|
from requests_toolbelt import MultipartEncoder
|
26
24
|
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
from openrelik_api_client.api_client import APIClient
|
27
27
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
api_version="v1",
|
49
|
-
):
|
50
|
-
self.base_url = f"{api_server_url}/api/{api_version}"
|
51
|
-
self.session = TokenRefreshSession(api_server_url, api_key)
|
52
|
-
|
53
|
-
def get(self, endpoint, **kwargs):
|
54
|
-
"""Sends a GET request to the specified API endpoint."""
|
55
|
-
url = f"{self.base_url}{endpoint}"
|
56
|
-
return self.session.get(url, **kwargs)
|
57
|
-
|
58
|
-
def post(self, endpoint, data=None, json=None, **kwargs):
|
59
|
-
"""Sends a POST request to the specified API endpoint."""
|
60
|
-
url = f"{self.base_url}{endpoint}"
|
61
|
-
return self.session.post(url, data=data, json=json, **kwargs)
|
62
|
-
|
63
|
-
def put(self, endpoint, data=None, **kwargs):
|
64
|
-
"""Sends a PUT request to the specified API endpoint."""
|
65
|
-
url = f"{self.base_url}{endpoint}"
|
66
|
-
return self.session.put(url, data=data, **kwargs)
|
67
|
-
|
68
|
-
def patch(self, endpoint, data=None, json=None, **kwargs):
|
69
|
-
"""Sends a PATCH request to the specified API endpoint."""
|
70
|
-
url = f"{self.base_url}{endpoint}"
|
71
|
-
return self.session.patch(url, data=data, json=json, **kwargs)
|
72
|
-
|
73
|
-
def delete(self, endpoint, **kwargs):
|
74
|
-
"""Sends a DELETE request to the specified API endpoint."""
|
75
|
-
url = f"{self.base_url}{endpoint}"
|
76
|
-
return self.session.delete(url, **kwargs)
|
77
|
-
|
78
|
-
def get_config(self) -> dict[str, Any]:
|
79
|
-
"""Gets the current OpenRelik configuration."""
|
80
|
-
endpoint = f"{self.base_url}/config/system/"
|
81
|
-
response = self.session.get(endpoint)
|
28
|
+
|
29
|
+
class FilesAPI:
|
30
|
+
def __init__(self, api_client: "APIClient"):
|
31
|
+
super().__init__()
|
32
|
+
self.api_client = api_client
|
33
|
+
|
34
|
+
def get_file_metadata(self, file_id: int) -> dict[str, Any]:
|
35
|
+
"""Reads a file metadata.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
file_id: The ID of the file to get the metadata from.
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
A dictionary containing file metadata.
|
42
|
+
|
43
|
+
Raises:
|
44
|
+
HTTPError: If the API request failed.
|
45
|
+
"""
|
46
|
+
endpoint = f"{self.api_client.base_url}/files/{file_id}/"
|
47
|
+
response = self.api_client.session.get(endpoint)
|
82
48
|
response.raise_for_status()
|
83
49
|
return response.json()
|
84
50
|
|
51
|
+
def get_file_content(
|
52
|
+
self, file_id: int, max_file_size_bytes: int, return_type: str = "bytes"
|
53
|
+
) -> str | bytes | None:
|
54
|
+
"""Download the content of a file.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
file_id: The ID of the file to download.
|
58
|
+
max_file_size_bytes: Maximum file size to download in bytes.
|
59
|
+
return_type: The type of content to return. Can be "bytes" or "text".
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
The content of the file as bytes if return_type is "bytes", or as a string if
|
63
|
+
return_type is "text". Returns None if the file does not exist.
|
64
|
+
|
65
|
+
Raises:
|
66
|
+
RuntimeError: If the file is too large to download.
|
67
|
+
ValueError: If the return_type is not "bytes" or "text".
|
68
|
+
"""
|
69
|
+
# Guard against large files as this will read the entire file into memory.
|
70
|
+
# If downloading larger files than 100MB is needed, use the download_file method instead.
|
71
|
+
MAX_FILE_SIZE_GUARD = 100 * 1024 * 1024 # 100 MB
|
72
|
+
|
73
|
+
filesize = self.get_file_metadata(file_id).get("filesize")
|
74
|
+
if filesize > max_file_size_bytes or filesize > MAX_FILE_SIZE_GUARD:
|
75
|
+
raise RuntimeError("File too large to download")
|
76
|
+
|
77
|
+
endpoint = f"{self.api_client.base_url}/files/{file_id}/download_stream"
|
78
|
+
response = self.api_client.session.get(endpoint)
|
79
|
+
response.raise_for_status()
|
80
|
+
if return_type == "text":
|
81
|
+
return response.text
|
82
|
+
elif return_type == "bytes":
|
83
|
+
return response.content
|
84
|
+
else:
|
85
|
+
raise ValueError("Invalid return_type. Must be 'bytes' or 'text'.")
|
86
|
+
|
85
87
|
def download_file(self, file_id: int, filename: str) -> str | None:
|
86
88
|
"""Downloads a file from OpenRelik.
|
87
89
|
|
@@ -92,8 +94,9 @@ class APIClient:
|
|
92
94
|
Returns:
|
93
95
|
str: The path to the downloaded file.
|
94
96
|
"""
|
95
|
-
endpoint = f"{self.base_url}/files/{file_id}/download"
|
96
|
-
response = self.session.get(endpoint)
|
97
|
+
endpoint = f"{self.api_client.base_url}/files/{file_id}/download"
|
98
|
+
response = self.api_client.session.get(endpoint)
|
99
|
+
response.raise_for_status()
|
97
100
|
filename_prefix, extension = os.path.splitext(filename)
|
98
101
|
file = tempfile.NamedTemporaryFile(
|
99
102
|
mode="wb", prefix=f"{filename_prefix}", suffix=extension, delete=False
|
@@ -131,7 +134,9 @@ class APIClient:
|
|
131
134
|
raise FileNotFoundError(f"File {file_path} not found.")
|
132
135
|
|
133
136
|
if folder_id:
|
134
|
-
response = self.session.get(
|
137
|
+
response = self.api_client.session.get(
|
138
|
+
f"{self.api_client.base_url}/folders/{folder_id}"
|
139
|
+
)
|
135
140
|
if response.status_code == 404:
|
136
141
|
return file_id
|
137
142
|
|
@@ -157,8 +162,8 @@ class APIClient:
|
|
157
162
|
{"file": (file_path.name, chunk, "application/octet-stream")}
|
158
163
|
)
|
159
164
|
headers = {"Content-Type": encoder.content_type}
|
160
|
-
response = self.session.post(
|
161
|
-
f"{self.base_url}{endpoint}",
|
165
|
+
response = self.api_client.session.post(
|
166
|
+
f"{self.api_client.base_url}{endpoint}",
|
162
167
|
headers=headers,
|
163
168
|
data=encoder.to_string(),
|
164
169
|
params=params,
|
@@ -182,58 +187,30 @@ class APIClient:
|
|
182
187
|
|
183
188
|
return file_id
|
184
189
|
|
185
|
-
|
186
|
-
|
187
|
-
"""Custom session class that handles automatic token refresh on 401 responses."""
|
188
|
-
|
189
|
-
def __init__(self, api_server_url, api_key):
|
190
|
-
"""
|
191
|
-
Initializes the TokenRefreshSession with the API server URL and refresh token.
|
190
|
+
def get_sql_schemas(self, file_id: int) -> dict[str, Any]:
|
191
|
+
"""Retrieve tables and schemas for a supported SQL file.
|
192
192
|
|
193
193
|
Args:
|
194
|
-
|
195
|
-
|
194
|
+
file_id: The ID of the file to run the query against.
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
A dictionary containing the results.
|
196
198
|
"""
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
self.headers["x-openrelik-refresh-token"] = api_key
|
199
|
+
endpoint = f"{self.api_client.base_url}/files/{file_id}/sql/schemas/"
|
200
|
+
response = self.api_client.session.get(endpoint)
|
201
|
+
return response.json()
|
201
202
|
|
202
|
-
def
|
203
|
-
"""
|
203
|
+
def run_sql_query(self, file_id: int, query: str) -> dict[str, Any]:
|
204
|
+
"""Runs a SQL query against a supported SQL file.
|
204
205
|
|
205
206
|
Args:
|
206
|
-
|
207
|
-
|
208
|
-
**kwargs: Additional keyword arguments for the request.
|
207
|
+
file_id: The ID of the file to run the query against.
|
208
|
+
query: The SQL query to run.
|
209
209
|
|
210
210
|
Returns:
|
211
|
-
|
212
|
-
|
213
|
-
Raises:
|
214
|
-
Exception: If the token refresh fails.
|
211
|
+
A dictionary containing the query results.
|
215
212
|
"""
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
# Retry the original request with the new token
|
221
|
-
response = super().request(method, url, **kwargs)
|
222
|
-
else:
|
223
|
-
raise Exception("Token refresh failed")
|
224
|
-
|
225
|
-
return response
|
226
|
-
|
227
|
-
def _refresh_token(self) -> bool:
|
228
|
-
"""Refreshes the access token using the refresh token."""
|
229
|
-
refresh_url = f"{self.api_server_url}/auth/refresh"
|
230
|
-
try:
|
231
|
-
response = self.get(refresh_url)
|
232
|
-
response.raise_for_status()
|
233
|
-
# Update session headers with the new access token
|
234
|
-
new_access_token = response.json().get("new_access_token")
|
235
|
-
self.headers["x-openrelik-access-token"] = new_access_token
|
236
|
-
return True
|
237
|
-
except RequestException as e:
|
238
|
-
print(f"Failed to refresh token: {e}")
|
239
|
-
return False
|
213
|
+
endpoint = f"{self.api_client.base_url}/files/{file_id}/sql/query/"
|
214
|
+
request_body = {"query": query}
|
215
|
+
response = self.api_client.session.post(endpoint, json=request_body)
|
216
|
+
return response.json()
|
@@ -22,6 +22,47 @@ class FoldersAPI:
|
|
22
22
|
super().__init__()
|
23
23
|
self.api_client = api_client
|
24
24
|
|
25
|
+
def list_root_folders(
|
26
|
+
self, limit: int, pagination_metadata: bool = False
|
27
|
+
) -> list[dict[str, Any]]:
|
28
|
+
"""List root folders.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
limit: Maximum number of folders to return.
|
32
|
+
pagination_metadata: If True, include pagination metadata in the response.
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
A list of dictionaries containing folder metadata.
|
36
|
+
|
37
|
+
Raises:
|
38
|
+
HTTPError: If the API request failed.
|
39
|
+
"""
|
40
|
+
endpoint = f"{self.api_client.base_url}/folders/all/?page_size={limit}"
|
41
|
+
response = self.api_client.session.get(endpoint)
|
42
|
+
response.raise_for_status()
|
43
|
+
if pagination_metadata:
|
44
|
+
folders = response.json()
|
45
|
+
else:
|
46
|
+
folders = response.json().get("folders", [])
|
47
|
+
return folders
|
48
|
+
|
49
|
+
def list_folder(self, folder_id: int) -> list[dict[str, Any]]:
|
50
|
+
"""List files in a folder.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
folder_id: The ID of the folder to check.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
A list of dictionaries containing file metadata
|
57
|
+
|
58
|
+
Raises:
|
59
|
+
HTTPError: If the API request failed.
|
60
|
+
"""
|
61
|
+
endpoint = f"{self.api_client.base_url}/folders/{folder_id}/files/"
|
62
|
+
response = self.api_client.session.get(endpoint)
|
63
|
+
response.raise_for_status()
|
64
|
+
return response.json()
|
65
|
+
|
25
66
|
def create_root_folder(self, display_name: str) -> int | None:
|
26
67
|
"""Create a root folder.
|
27
68
|
|
@@ -56,12 +97,11 @@ class FoldersAPI:
|
|
56
97
|
Raises:
|
57
98
|
HTTPError: If the API request failed.
|
58
99
|
"""
|
59
|
-
# Corrected: Use the passed folder_id in the endpoint URL
|
60
100
|
endpoint = f"{self.api_client.base_url}/folders/{folder_id}/folders/"
|
61
101
|
data = {"display_name": display_name}
|
62
102
|
response = self.api_client.session.post(endpoint, json=data)
|
63
103
|
response.raise_for_status()
|
64
|
-
|
104
|
+
|
65
105
|
new_folder_id = None
|
66
106
|
if response.status_code == 201:
|
67
107
|
new_folder_id = response.json().get("id")
|
@@ -84,9 +124,7 @@ class FoldersAPI:
|
|
84
124
|
response.raise_for_status()
|
85
125
|
return response.status_code == 200
|
86
126
|
|
87
|
-
def update_folder(
|
88
|
-
self, folder_id: int, folder_data: dict[str, Any]
|
89
|
-
) -> dict[str, Any] | None:
|
127
|
+
def update_folder(self, folder_id: int, folder_data: dict[str, Any]) -> dict[str, Any] | None:
|
90
128
|
"""Updates an existing folder.
|
91
129
|
|
92
130
|
Args:
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright 2024 Google LLC
|
1
|
+
# Copyright 2024-2025 Google LLC
|
2
2
|
#
|
3
3
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
4
|
# you may not use this file except in compliance with the License.
|
@@ -25,7 +25,7 @@ class WorkflowsAPI:
|
|
25
25
|
self.folders_url = f"{self.api_client.base_url}/folders"
|
26
26
|
|
27
27
|
def create_workflow(
|
28
|
-
self, folder_id: int, file_ids: list, template_id: int = None
|
28
|
+
self, folder_id: int, file_ids: list, template_id: int = None, template_params: dict = {}
|
29
29
|
) -> int | None:
|
30
30
|
"""Creates a new workflow.
|
31
31
|
|
@@ -46,6 +46,7 @@ class WorkflowsAPI:
|
|
46
46
|
"folder_id": folder_id,
|
47
47
|
"file_ids": file_ids,
|
48
48
|
"template_id": template_id,
|
49
|
+
"template_params": template_params,
|
49
50
|
}
|
50
51
|
response = self.api_client.session.post(endpoint, json=data)
|
51
52
|
response.raise_for_status()
|
@@ -157,3 +158,21 @@ class WorkflowsAPI:
|
|
157
158
|
if response.status_code == 200:
|
158
159
|
workflow = response.json()
|
159
160
|
return workflow
|
161
|
+
|
162
|
+
def get_workflow_report(self, workflow_id: int) -> dict[str, Any] | None:
|
163
|
+
"""Retrieves a workflow report.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
workflow_id: The ID of the workflow to retrieve the report from.
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
The workflow report data.
|
170
|
+
|
171
|
+
Raises:
|
172
|
+
HTTPError: If the API request failed.
|
173
|
+
"""
|
174
|
+
endpoint = f"{self.api_client.base_url}/workflows/{workflow_id}/report/"
|
175
|
+
response = self.api_client.session.get(endpoint)
|
176
|
+
response.raise_for_status()
|
177
|
+
if response.status_code == 200:
|
178
|
+
return response.json()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "openrelik-api-client"
|
3
|
-
version = "0.
|
3
|
+
version = "0.5.0"
|
4
4
|
description = "API client that automatically handles token refresh"
|
5
5
|
authors = ["Johan Berggren <jberggren@gmail.com>"]
|
6
6
|
readme = "README.md"
|
@@ -10,6 +10,17 @@ python = "^3.10"
|
|
10
10
|
requests = "^2.32.3"
|
11
11
|
requests_toolbelt = "^1.0.0"
|
12
12
|
|
13
|
+
[tool.poetry.group.dev.dependencies]
|
14
|
+
pylint = "^3.1.0"
|
15
|
+
pytest = "^8.0.2"
|
16
|
+
pytest-mock = "^3.14.0"
|
17
|
+
pytest-cov = "^6.0.0"
|
18
|
+
|
19
|
+
[tool.pytest.ini_options]
|
20
|
+
python_files = "*_test.py"
|
21
|
+
python_classes = "Test*"
|
22
|
+
python_functions = "test_*"
|
23
|
+
|
13
24
|
[build-system]
|
14
25
|
requires = ["poetry-core"]
|
15
26
|
build-backend = "poetry.core.masonry.api"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|