openrelik-api-client 0.3.0__py3-none-any.whl → 0.4.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.
@@ -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 math
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
- from requests_toolbelt import MultipartEncoder
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
- """Gets the current OpenRelik configuration."""
80
- endpoint = f"{self.base_url}/config/system/"
81
- response = self.session.get(endpoint)
82
- response.raise_for_status()
83
- return response.json()
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
- endpoint = f"{self.base_url}/files/{file_id}/download"
96
- response = self.session.get(endpoint)
97
- filename_prefix, extension = os.path.splitext(filename)
98
- file = tempfile.NamedTemporaryFile(
99
- mode="wb", prefix=f"{filename_prefix}", suffix=extension, delete=False
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
- file.write(response.content)
102
- file.close()
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
- MAX_CHUNK_RETRIES = 10 # Maximum number of retries for chunk upload
119
- CHUNK_RETRY_INTERVAL = 0.5 # seconds
120
-
121
- file_id = None
122
- response = None
123
- endpoint = "/files/upload"
124
- chunk_size = 10 * 1024 * 1024 # 10 MB
125
- resumableTotalChunks = 0
126
- resumableChunkNumber = 0
127
- resumableIdentifier = uuid4().hex
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 Exception("Token refresh failed")
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()
@@ -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
- # Corrected: assign to a different variable or use the return directly
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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openrelik-api-client
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: API client that automatically handles token refresh
5
5
  Author: Johan Berggren
6
6
  Author-email: jberggren@gmail.com
@@ -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=5DjpNuqGfbLz-glWPqqIZpL6wtHtt2DWsqG-EpFDeRw,5510
8
+ openrelik_api_client-0.4.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
9
+ openrelik_api_client-0.4.0.dist-info/METADATA,sha256=65UwGhJtWFuEc_Hcfpf7ehjdUbAU6EPsfyxYc8eQQgU,2114
10
+ openrelik_api_client-0.4.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
11
+ openrelik_api_client-0.4.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,,