openrelik-api-client 0.3.0__py3-none-any.whl → 0.5.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.
- openrelik_api_client/api_client.py +43 -108
- openrelik_api_client/configs.py +31 -0
- openrelik_api_client/files.py +216 -0
- openrelik_api_client/folders.py +43 -5
- openrelik_api_client/workflows.py +21 -2
- {openrelik_api_client-0.3.0.dist-info → openrelik_api_client-0.5.0.dist-info}/METADATA +1 -1
- openrelik_api_client-0.5.0.dist-info/RECORD +11 -0
- openrelik_api_client-0.3.0.dist-info/RECORD +0 -9
- {openrelik_api_client-0.3.0.dist-info → openrelik_api_client-0.5.0.dist-info}/LICENSE +0 -0
- {openrelik_api_client-0.3.0.dist-info → openrelik_api_client-0.5.0.dist-info}/WHEEL +0 -0
@@ -12,17 +12,14 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
import
|
16
|
-
import os
|
17
|
-
import tempfile
|
18
|
-
import time
|
19
|
-
from pathlib import Path
|
15
|
+
import warnings
|
20
16
|
from typing import Any
|
21
|
-
from uuid import uuid4
|
22
17
|
|
23
18
|
import requests
|
24
19
|
from requests.exceptions import RequestException
|
25
|
-
|
20
|
+
|
21
|
+
from .configs import ConfigsAPI
|
22
|
+
from .files import FilesAPI
|
26
23
|
|
27
24
|
|
28
25
|
class APIClient:
|
@@ -76,111 +73,43 @@ class APIClient:
|
|
76
73
|
return self.session.delete(url, **kwargs)
|
77
74
|
|
78
75
|
def get_config(self) -> dict[str, Any]:
|
79
|
-
"""
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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()
|
84
87
|
|
85
88
|
def download_file(self, file_id: int, filename: str) -> str | None:
|
86
|
-
"""Downloads a file from OpenRelik.
|
87
|
-
|
88
|
-
Args:
|
89
|
-
file_id: The ID of the file to download.
|
90
|
-
filename: The name of the file to download.
|
91
|
-
|
92
|
-
Returns:
|
93
|
-
str: The path to the downloaded file.
|
94
89
|
"""
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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,
|
100
97
|
)
|
101
|
-
|
102
|
-
|
103
|
-
return file.name
|
98
|
+
files_api = FilesAPI(self)
|
99
|
+
return files_api.download_file(file_id, filename)
|
104
100
|
|
105
101
|
def upload_file(self, file_path: str, folder_id: int) -> int | None:
|
106
|
-
"""Uploads a file to the server.
|
107
|
-
|
108
|
-
Args:
|
109
|
-
file_path: File contents.
|
110
|
-
folder_id: An existing OpenRelik folder identifier.
|
111
|
-
|
112
|
-
Returns:
|
113
|
-
file_id of the uploaded file or None otherwise.
|
114
|
-
|
115
|
-
Raise:
|
116
|
-
FileNotFoundError: if file_path is not found.
|
117
102
|
"""
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
file_path = Path(file_path)
|
129
|
-
resumableFilename = file_path.name
|
130
|
-
if not file_path.exists():
|
131
|
-
raise FileNotFoundError(f"File {file_path} not found.")
|
132
|
-
|
133
|
-
if folder_id:
|
134
|
-
response = self.session.get(f"{self.base_url}/folders/{folder_id}")
|
135
|
-
if response.status_code == 404:
|
136
|
-
return file_id
|
137
|
-
|
138
|
-
with open(file_path, "rb") as fh:
|
139
|
-
total_size = Path(file_path).stat().st_size
|
140
|
-
resumableTotalChunks = math.ceil(total_size / chunk_size)
|
141
|
-
while chunk := fh.read(chunk_size):
|
142
|
-
resumableChunkNumber += 1
|
143
|
-
retry_count = 0
|
144
|
-
while retry_count < MAX_CHUNK_RETRIES:
|
145
|
-
params = {
|
146
|
-
"resumableRelativePath": resumableFilename,
|
147
|
-
"resumableTotalSize": total_size,
|
148
|
-
"resumableCurrentChunkSize": len(chunk),
|
149
|
-
"resumableChunkSize": chunk_size,
|
150
|
-
"resumableChunkNumber": resumableChunkNumber,
|
151
|
-
"resumableTotalChunks": resumableTotalChunks,
|
152
|
-
"resumableIdentifier": resumableIdentifier,
|
153
|
-
"resumableFilename": resumableFilename,
|
154
|
-
"folder_id": folder_id,
|
155
|
-
}
|
156
|
-
encoder = MultipartEncoder(
|
157
|
-
{"file": (file_path.name, chunk, "application/octet-stream")}
|
158
|
-
)
|
159
|
-
headers = {"Content-Type": encoder.content_type}
|
160
|
-
response = self.session.post(
|
161
|
-
f"{self.base_url}{endpoint}",
|
162
|
-
headers=headers,
|
163
|
-
data=encoder.to_string(),
|
164
|
-
params=params,
|
165
|
-
)
|
166
|
-
if response.status_code == 200 or response.status_code == 201:
|
167
|
-
# Success, move to the next chunk
|
168
|
-
break
|
169
|
-
elif response.status_code == 503:
|
170
|
-
# Server has issue saving the chunk, retry the upload.
|
171
|
-
retry_count += 1
|
172
|
-
time.sleep(CHUNK_RETRY_INTERVAL)
|
173
|
-
elif response.status_code == 429:
|
174
|
-
# Rate limit exceeded, cancel the upload and raise an error.
|
175
|
-
raise RuntimeError("Upload failed, maximum retries exceeded")
|
176
|
-
else:
|
177
|
-
# Other errors, cancel the upload and raise an error.
|
178
|
-
raise RuntimeError("Upload failed")
|
179
|
-
|
180
|
-
if response and response.status_code == 201:
|
181
|
-
file_id = response.json().get("id")
|
182
|
-
|
183
|
-
return file_id
|
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)
|
184
113
|
|
185
114
|
|
186
115
|
class TokenRefreshSession(requests.Session):
|
@@ -216,17 +145,23 @@ class TokenRefreshSession(requests.Session):
|
|
216
145
|
response = super().request(method, url, **kwargs)
|
217
146
|
|
218
147
|
if response.status_code == 401:
|
219
|
-
if self._refresh_token():
|
148
|
+
if self._refresh_token(url):
|
220
149
|
# Retry the original request with the new token
|
221
150
|
response = super().request(method, url, **kwargs)
|
222
151
|
else:
|
223
|
-
raise
|
152
|
+
raise RuntimeError("API key has expired")
|
224
153
|
|
225
154
|
return response
|
226
155
|
|
227
|
-
def _refresh_token(self) -> bool:
|
156
|
+
def _refresh_token(self, requested_url: str) -> bool:
|
228
157
|
"""Refreshes the access token using the refresh token."""
|
229
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
|
+
|
230
165
|
try:
|
231
166
|
response = self.get(refresh_url)
|
232
167
|
response.raise_for_status()
|
@@ -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()
|
@@ -0,0 +1,216 @@
|
|
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
|
+
import math
|
16
|
+
import os
|
17
|
+
import tempfile
|
18
|
+
import time
|
19
|
+
from pathlib import Path
|
20
|
+
from typing import TYPE_CHECKING, Any
|
21
|
+
from uuid import uuid4
|
22
|
+
|
23
|
+
from requests_toolbelt import MultipartEncoder
|
24
|
+
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
from openrelik_api_client.api_client import APIClient
|
27
|
+
|
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)
|
48
|
+
response.raise_for_status()
|
49
|
+
return response.json()
|
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
|
+
|
87
|
+
def download_file(self, file_id: int, filename: str) -> str | None:
|
88
|
+
"""Downloads a file from OpenRelik.
|
89
|
+
|
90
|
+
Args:
|
91
|
+
file_id: The ID of the file to download.
|
92
|
+
filename: The name of the file to download.
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
str: The path to the downloaded file.
|
96
|
+
"""
|
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()
|
100
|
+
filename_prefix, extension = os.path.splitext(filename)
|
101
|
+
file = tempfile.NamedTemporaryFile(
|
102
|
+
mode="wb", prefix=f"{filename_prefix}", suffix=extension, delete=False
|
103
|
+
)
|
104
|
+
file.write(response.content)
|
105
|
+
file.close()
|
106
|
+
return file.name
|
107
|
+
|
108
|
+
def upload_file(self, file_path: str, folder_id: int) -> int | None:
|
109
|
+
"""Uploads a file to the server.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
file_path: File contents.
|
113
|
+
folder_id: An existing OpenRelik folder identifier.
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
file_id of the uploaded file or None otherwise.
|
117
|
+
|
118
|
+
Raise:
|
119
|
+
FileNotFoundError: if file_path is not found.
|
120
|
+
"""
|
121
|
+
MAX_CHUNK_RETRIES = 10 # Maximum number of retries for chunk upload
|
122
|
+
CHUNK_RETRY_INTERVAL = 0.5 # seconds
|
123
|
+
|
124
|
+
file_id = None
|
125
|
+
response = None
|
126
|
+
endpoint = "/files/upload"
|
127
|
+
chunk_size = 10 * 1024 * 1024 # 10 MB
|
128
|
+
resumableTotalChunks = 0
|
129
|
+
resumableChunkNumber = 0
|
130
|
+
resumableIdentifier = uuid4().hex
|
131
|
+
file_path = Path(file_path)
|
132
|
+
resumableFilename = file_path.name
|
133
|
+
if not file_path.exists():
|
134
|
+
raise FileNotFoundError(f"File {file_path} not found.")
|
135
|
+
|
136
|
+
if folder_id:
|
137
|
+
response = self.api_client.session.get(
|
138
|
+
f"{self.api_client.base_url}/folders/{folder_id}"
|
139
|
+
)
|
140
|
+
if response.status_code == 404:
|
141
|
+
return file_id
|
142
|
+
|
143
|
+
with open(file_path, "rb") as fh:
|
144
|
+
total_size = Path(file_path).stat().st_size
|
145
|
+
resumableTotalChunks = math.ceil(total_size / chunk_size)
|
146
|
+
while chunk := fh.read(chunk_size):
|
147
|
+
resumableChunkNumber += 1
|
148
|
+
retry_count = 0
|
149
|
+
while retry_count < MAX_CHUNK_RETRIES:
|
150
|
+
params = {
|
151
|
+
"resumableRelativePath": resumableFilename,
|
152
|
+
"resumableTotalSize": total_size,
|
153
|
+
"resumableCurrentChunkSize": len(chunk),
|
154
|
+
"resumableChunkSize": chunk_size,
|
155
|
+
"resumableChunkNumber": resumableChunkNumber,
|
156
|
+
"resumableTotalChunks": resumableTotalChunks,
|
157
|
+
"resumableIdentifier": resumableIdentifier,
|
158
|
+
"resumableFilename": resumableFilename,
|
159
|
+
"folder_id": folder_id,
|
160
|
+
}
|
161
|
+
encoder = MultipartEncoder(
|
162
|
+
{"file": (file_path.name, chunk, "application/octet-stream")}
|
163
|
+
)
|
164
|
+
headers = {"Content-Type": encoder.content_type}
|
165
|
+
response = self.api_client.session.post(
|
166
|
+
f"{self.api_client.base_url}{endpoint}",
|
167
|
+
headers=headers,
|
168
|
+
data=encoder.to_string(),
|
169
|
+
params=params,
|
170
|
+
)
|
171
|
+
if response.status_code == 200 or response.status_code == 201:
|
172
|
+
# Success, move to the next chunk
|
173
|
+
break
|
174
|
+
elif response.status_code == 503:
|
175
|
+
# Server has issue saving the chunk, retry the upload.
|
176
|
+
retry_count += 1
|
177
|
+
time.sleep(CHUNK_RETRY_INTERVAL)
|
178
|
+
elif response.status_code == 429:
|
179
|
+
# Rate limit exceeded, cancel the upload and raise an error.
|
180
|
+
raise RuntimeError("Upload failed, maximum retries exceeded")
|
181
|
+
else:
|
182
|
+
# Other errors, cancel the upload and raise an error.
|
183
|
+
raise RuntimeError("Upload failed")
|
184
|
+
|
185
|
+
if response and response.status_code == 201:
|
186
|
+
file_id = response.json().get("id")
|
187
|
+
|
188
|
+
return file_id
|
189
|
+
|
190
|
+
def get_sql_schemas(self, file_id: int) -> dict[str, Any]:
|
191
|
+
"""Retrieve tables and schemas for a supported SQL file.
|
192
|
+
|
193
|
+
Args:
|
194
|
+
file_id: The ID of the file to run the query against.
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
A dictionary containing the results.
|
198
|
+
"""
|
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()
|
202
|
+
|
203
|
+
def run_sql_query(self, file_id: int, query: str) -> dict[str, Any]:
|
204
|
+
"""Runs a SQL query against a supported SQL file.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
file_id: The ID of the file to run the query against.
|
208
|
+
query: The SQL query to run.
|
209
|
+
|
210
|
+
Returns:
|
211
|
+
A dictionary containing the query results.
|
212
|
+
"""
|
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()
|
openrelik_api_client/folders.py
CHANGED
@@ -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()
|
@@ -0,0 +1,11 @@
|
|
1
|
+
openrelik_api_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
openrelik_api_client/api_client.py,sha256=rRRMAnEYSO2Gsy7SFrCA2a4LmpIvqo7uXYDleWqWk7Y,6380
|
3
|
+
openrelik_api_client/configs.py,sha256=ns0AF6i9vZq8M-1EKEF4xZ-IpcvDPbTFxn1WMAe_PnU,1127
|
4
|
+
openrelik_api_client/files.py,sha256=0lWUsMIjUGsz9CGQJFchcfq3myBSaxSZCKPJ1oyMgWQ,8444
|
5
|
+
openrelik_api_client/folders.py,sha256=6Hix2G4WoRPBMQu7HTeV_ZS_lZTj8pZW57NW80riZp4,8118
|
6
|
+
openrelik_api_client/groups.py,sha256=zhR2oq9_PG0ZmFgVHf62T7KWeg7h6mJmXOABScOJGyU,6116
|
7
|
+
openrelik_api_client/workflows.py,sha256=BwIBSjS0krsJWglPiFdAAtcwRMqNmtWDyz9H7UDgHWE,6187
|
8
|
+
openrelik_api_client-0.5.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
9
|
+
openrelik_api_client-0.5.0.dist-info/METADATA,sha256=pcSwYPyaRjtqxonmu6mMPr6YpnKdw4JDkerxXSnYgw8,2114
|
10
|
+
openrelik_api_client-0.5.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
11
|
+
openrelik_api_client-0.5.0.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
openrelik_api_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
openrelik_api_client/api_client.py,sha256=MjkiGJuU_t9S_n0aQSwPPRpgo1NpjVViC7WFx-PCikg,9130
|
3
|
-
openrelik_api_client/folders.py,sha256=RtL94DOSP72to_kLDF9HP94zQfXKui8xoLT-OSdHSxw,6943
|
4
|
-
openrelik_api_client/groups.py,sha256=zhR2oq9_PG0ZmFgVHf62T7KWeg7h6mJmXOABScOJGyU,6116
|
5
|
-
openrelik_api_client/workflows.py,sha256=5DjpNuqGfbLz-glWPqqIZpL6wtHtt2DWsqG-EpFDeRw,5510
|
6
|
-
openrelik_api_client-0.3.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
7
|
-
openrelik_api_client-0.3.0.dist-info/METADATA,sha256=2OMDYJtxbMFRWnDyM_bhUrz-GHDxqDvDVYenzfFMHCw,2114
|
8
|
-
openrelik_api_client-0.3.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
9
|
-
openrelik_api_client-0.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|