openrelik-api-client 0.2.3__tar.gz → 0.2.4__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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: openrelik-api-client
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: API client that automatically handles token refresh
5
5
  Author: Johan Berggren
6
6
  Author-email: jberggren@gmail.com
@@ -9,6 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
12
13
  Requires-Dist: requests (>=2.32.3,<3.0.0)
13
14
  Requires-Dist: requests_toolbelt (>=1.0.0,<2.0.0)
14
15
  Description-Content-Type: text/markdown
@@ -12,14 +12,17 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- import tempfile
16
- from requests_toolbelt import MultipartEncoder
17
- from requests.exceptions import RequestException
18
- import requests
15
+ import math
19
16
  import os
17
+ import tempfile
18
+ import time
20
19
  from pathlib import Path
20
+ from typing import Any
21
21
  from uuid import uuid4
22
- import math
22
+
23
+ import requests
24
+ from requests.exceptions import RequestException
25
+ from requests_toolbelt import MultipartEncoder
23
26
 
24
27
 
25
28
  class APIClient:
@@ -72,6 +75,13 @@ class APIClient:
72
75
  url = f"{self.base_url}{endpoint}"
73
76
  return self.session.delete(url, **kwargs)
74
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)
82
+ response.raise_for_status()
83
+ return response.json()
84
+
75
85
  def download_file(self, file_id: int, filename: str) -> str | None:
76
86
  """Downloads a file from OpenRelik.
77
87
 
@@ -92,8 +102,7 @@ class APIClient:
92
102
  file.close()
93
103
  return file.name
94
104
 
95
- def upload_file(
96
- self, file_path: str, folder_id: int) -> int | None:
105
+ def upload_file(self, file_path: str, folder_id: int) -> int | None:
97
106
  """Uploads a file to the server.
98
107
 
99
108
  Args:
@@ -106,10 +115,13 @@ class APIClient:
106
115
  Raise:
107
116
  FileNotFoundError: if file_path is not found.
108
117
  """
118
+ MAX_CHUNK_RETRIES = 10 # Maximum number of retries for chunk upload
119
+ CHUNK_RETRY_INTERVAL = 0.5 # seconds
120
+
109
121
  file_id = None
110
122
  response = None
111
123
  endpoint = "/files/upload"
112
- chunk_size = 1024000 # 1 MB
124
+ chunk_size = 10 * 1024 * 1024 # 10 MB
113
125
  resumableTotalChunks = 0
114
126
  resumableChunkNumber = 0
115
127
  resumableIdentifier = uuid4().hex
@@ -128,30 +140,46 @@ class APIClient:
128
140
  resumableTotalChunks = math.ceil(total_size / chunk_size)
129
141
  while chunk := fh.read(chunk_size):
130
142
  resumableChunkNumber += 1
131
- params = {
132
- "resumableRelativePath": resumableFilename,
133
- "resumableTotalSize": total_size,
134
- "resumableCurrentChunkSize": chunk_size,
135
- "resumableChunkSize": chunk_size,
136
- "resumableChunkNumber": resumableChunkNumber,
137
- "resumableTotalChunks": resumableTotalChunks,
138
- "resumableIdentifier": resumableIdentifier,
139
- "resumableFilename": resumableFilename,
140
- "folder_id": folder_id
141
- }
142
- encoder = MultipartEncoder(
143
- {"file": (file_path.name, chunk,
144
- "application/octet-stream")}
145
- )
146
- headers = {"Content-Type": encoder.content_type}
147
- response = self.session.post(
148
- f"{self.base_url}{endpoint}",
149
- headers=headers,
150
- data=encoder.to_string(),
151
- params=params,
152
- )
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
+
153
180
  if response and response.status_code == 201:
154
- file_id = response.json().get('id')
181
+ file_id = response.json().get("id")
182
+
155
183
  return file_id
156
184
 
157
185
 
@@ -171,7 +199,7 @@ class TokenRefreshSession(requests.Session):
171
199
  if api_key:
172
200
  self.headers["x-openrelik-refresh-token"] = api_key
173
201
 
174
- def request(self, method, url, **kwargs):
202
+ def request(self, method: str, url: str, **kwargs: dict[str, Any]) -> requests.Response:
175
203
  """Intercepts the request to handle token expiration.
176
204
 
177
205
  Args:
@@ -196,7 +224,7 @@ class TokenRefreshSession(requests.Session):
196
224
 
197
225
  return response
198
226
 
199
- def _refresh_token(self):
227
+ def _refresh_token(self) -> bool:
200
228
  """Refreshes the access token using the refresh token."""
201
229
  refresh_url = f"{self.api_server_url}/auth/refresh"
202
230
  try:
@@ -0,0 +1,184 @@
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
+ from typing import Any
16
+
17
+ from openrelik_api_client.api_client import APIClient
18
+
19
+
20
+ class FoldersAPI:
21
+ def __init__(self, api_client: APIClient):
22
+ super().__init__()
23
+ self.api_client = api_client
24
+
25
+ def create_root_folder(self, display_name: str) -> int | None:
26
+ """Create a root folder.
27
+
28
+ Args:
29
+ display_name (str): Folder display name.
30
+
31
+ Returns:
32
+ int: Folder ID for the new root folder, or None otherwise.
33
+
34
+ Raises:
35
+ HTTPError: If the API request failed.
36
+ """
37
+ folder_id = None
38
+ endpoint = f"{self.api_client.base_url}/folders/"
39
+ params = {"display_name": display_name}
40
+ response = self.api_client.session.post(endpoint, json=params)
41
+ response.raise_for_status()
42
+ if response.status_code == 201:
43
+ folder_id = response.json().get("id")
44
+ return folder_id
45
+
46
+ def create_subfolder(self, folder_id: int, display_name: str) -> int | None:
47
+ """Create a subfolder within the given folder ID.
48
+
49
+ Args:
50
+ folder_id: The ID of the parent folder.
51
+ display_name: The name of the subfolder to check.
52
+
53
+ Returns:
54
+ int: Folder ID for the new root folder, or None.
55
+
56
+ Raises:
57
+ HTTPError: If the API request failed.
58
+ """
59
+ # Corrected: Use the passed folder_id in the endpoint URL
60
+ endpoint = f"{self.api_client.base_url}/folders/{folder_id}/folders/"
61
+ data = {"display_name": display_name}
62
+ response = self.api_client.session.post(endpoint, json=data)
63
+ response.raise_for_status()
64
+ # Corrected: assign to a different variable or use the return directly
65
+ new_folder_id = None
66
+ if response.status_code == 201:
67
+ new_folder_id = response.json().get("id")
68
+ return new_folder_id
69
+
70
+ def folder_exists(self, folder_id: int) -> bool:
71
+ """Checks if a folder with the given ID exists.
72
+
73
+ Args:
74
+ folder_id: The ID of the folder to check.
75
+
76
+ Returns:
77
+ True if the folder exists, False otherwise.
78
+
79
+ Raises:
80
+ HTTPError: If the API request failed.
81
+ """
82
+ endpoint = f"{self.api_client.base_url}/folders/{folder_id}"
83
+ response = self.api_client.session.get(endpoint)
84
+ response.raise_for_status()
85
+ return response.status_code == 200
86
+
87
+ def update_folder(
88
+ self, folder_id: int, folder_data: dict[str, Any]
89
+ ) -> dict[str, Any] | None:
90
+ """Updates an existing folder.
91
+
92
+ Args:
93
+ folder_id: The ID of the folder to update.
94
+ folder_data: The updated folder data.
95
+
96
+ Returns:
97
+ The updated folder data, or None.
98
+
99
+ Raises:
100
+ HTTPError: If the API request failed.
101
+ """
102
+ endpoint = f"{self.api_client.base_url}/folders/{folder_id}"
103
+ response = self.api_client.session.patch(endpoint, json=folder_data)
104
+ response.raise_for_status()
105
+ return response.json()
106
+
107
+ def delete_folder(self, folder_id: int) -> bool:
108
+ """Deletes an existing folder.
109
+
110
+ Args:
111
+ folder_id: The ID of the folder to update.
112
+
113
+ Returns:
114
+ True if the request was successful.
115
+
116
+ Raises:
117
+ HTTPError: If the API request failed.
118
+ """
119
+ endpoint = f"{self.api_client.base_url}/folders/{folder_id}"
120
+ response = self.api_client.session.delete(endpoint)
121
+ response.raise_for_status()
122
+ return response.status_code == 204
123
+
124
+ def share_folder(
125
+ self,
126
+ folder_id: int,
127
+ user_names: list[str] | None = None,
128
+ group_names: list[str] | None = None,
129
+ user_ids: list[int] | None = None,
130
+ group_ids: list[int] | None = None,
131
+ user_role: str | None = None,
132
+ group_role: str | None = None,
133
+ ) -> dict[str, Any] | None:
134
+ """Shares a folder with specified users and/or groups.
135
+
136
+ The server expects all fields in the FolderShareRequest schema to be present.
137
+ If user_role or group_role are not provided, they default to "viewer".
138
+
139
+ Args:
140
+ folder_id: The ID of the folder to share.
141
+ user_names: A list of usernames to share the folder with.
142
+ group_names: A list of group names to share the folder with.
143
+ user_ids: A list of user IDs to share the folder with.
144
+ group_ids: A list of group IDs to share the folder with.
145
+ user_role: The role to assign to the specified users (e.g., "viewer", "editor").
146
+ Defaults to "viewer" if not provided.
147
+ group_role: The role to assign to the specified groups (e.g., "viewer", "editor").
148
+ Defaults to "viewer" if not provided.
149
+
150
+ Returns:
151
+ None if the sharing operation was successful. The server returns a null body
152
+ which is deserialized to None.
153
+
154
+ Raises:
155
+ requests.exceptions.HTTPError: If the API request failed (e.g., folder not found,
156
+ invalid role, permission denied).
157
+ """
158
+ endpoint = f"{self.api_client.base_url}/folders/{folder_id}/roles"
159
+
160
+ # Ensure lists are not None for the payload
161
+ payload_user_ids = user_ids if user_ids is not None else []
162
+ payload_user_names = user_names if user_names is not None else []
163
+ payload_group_ids = group_ids if group_ids is not None else []
164
+ payload_group_names = group_names if group_names is not None else []
165
+
166
+ # Default roles to "viewer" if not provided, as the server schema requires these fields.
167
+ payload_user_role = user_role if user_role is not None else "viewer"
168
+ payload_group_role = group_role if group_role is not None else "viewer"
169
+
170
+ data = {
171
+ "user_ids": payload_user_ids,
172
+ "user_names": payload_user_names,
173
+ "group_ids": payload_group_ids,
174
+ "group_names": payload_group_names,
175
+ "user_role": payload_user_role,
176
+ "group_role": payload_group_role,
177
+ }
178
+ try:
179
+ response = self.api_client.session.post(endpoint, json=data)
180
+ response.raise_for_status()
181
+ return response.json()
182
+ except Exception as e:
183
+ print(f"An error occurred: {e.response.json()}")
184
+ return None
@@ -0,0 +1,190 @@
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 Any
16
+
17
+ from openrelik_api_client.api_client import APIClient
18
+
19
+
20
+ class GroupsAPI:
21
+ """
22
+ Manages groups for Oppenrelik.
23
+ Provides functionalities for creating, retrieving, updating,
24
+ deleting groups, and managing group memberships.
25
+ """
26
+
27
+ def __init__(self, api_client: APIClient):
28
+ super().__init__()
29
+ self.api_client = api_client
30
+ self.groups_url = f"{self.api_client.base_url}/groups"
31
+
32
+ def create_group(self, name: str, description: str = "") -> dict[str, Any] | None:
33
+ """
34
+ Creates a new group.
35
+
36
+ Args:
37
+ name: The name of the group.
38
+ description: An optional description for the group.
39
+
40
+ Returns:
41
+ A dictionary representing the newly created group if successful,
42
+ None otherwise.
43
+
44
+ Raises:
45
+ ValueError: If the group name is empty.
46
+ requests.exceptions.HTTPError: If the API request failed.
47
+ """
48
+ if not name:
49
+ raise ValueError("Group name cannot be empty.")
50
+
51
+ endpoint = f"{self.groups_url}/"
52
+ data = {
53
+ "name": name,
54
+ "description": description,
55
+ }
56
+ response = self.api_client.session.post(endpoint, json=data)
57
+ response.raise_for_status()
58
+ if response.status_code == 201:
59
+ return response.json()
60
+ return None
61
+
62
+ def remove_group(self, group_name: str) -> bool:
63
+ """
64
+ Removes a group by its name.
65
+
66
+ Args:
67
+ group_name: The name of the group to remove.
68
+
69
+ Returns:
70
+ True if the group was successfully removed.
71
+
72
+ Raises:
73
+ requests.exceptions.HTTPError: If the API request failed.
74
+ """
75
+ endpoint = f"{self.groups_url}/{group_name}"
76
+ response = self.api_client.session.delete(endpoint)
77
+ response.raise_for_status()
78
+ return response.status_code == 204
79
+
80
+ def list_group_members(self, group_name: str) -> dict[str, Any] | None:
81
+ """
82
+ Retrieves a group by its name.
83
+
84
+ Args:
85
+ group_name: The name of the group to retrieve.
86
+
87
+ Returns:
88
+ A dictionary representing the group if found, None otherwise.
89
+
90
+ Raises:
91
+ requests.exceptions.HTTPError: If the API request failed (e.g., 404 Not Found).
92
+ """
93
+ endpoint = f"{self.groups_url}/{group_name}/users"
94
+ response = self.api_client.session.get(endpoint)
95
+ response.raise_for_status()
96
+ if response.status_code == 200:
97
+ return response.json()
98
+ return None
99
+
100
+ def list_groups(self) -> list[dict[str, Any]]:
101
+ """
102
+ Lists all existing groups.
103
+
104
+ Returns:
105
+ A list of dictionaries, where each dictionary represents a group.
106
+
107
+ Raises:
108
+ requests.exceptions.HTTPError: If the API request failed.
109
+ """
110
+ endpoint = f"{self.groups_url}/"
111
+ response = self.api_client.session.get(endpoint)
112
+ response.raise_for_status()
113
+ if response.status_code == 200:
114
+ return response.json()
115
+ return []
116
+
117
+ def delete_group(self, group_name: str) -> bool:
118
+ """
119
+ Deletes a group by its name.
120
+
121
+ Args:
122
+ group_name: The name of the group to delete.
123
+
124
+ Returns:
125
+ True if the group was successfully deleted.
126
+
127
+ Raises:
128
+ requests.exceptions.HTTPError: If the API request failed.
129
+ """
130
+ endpoint = f"{self.groups_url}/{group_name}"
131
+ response = self.api_client.session.delete(endpoint)
132
+ response.raise_for_status()
133
+ return response.status_code == 204
134
+
135
+ def add_users_to_group(self, group_name: str, users: list[str]) -> bool:
136
+ """
137
+ Adds a user to a group.
138
+
139
+ Args:
140
+ group_name: The name of the group.
141
+ users: The users to add.
142
+
143
+ Returns:
144
+ List of users that were added.
145
+
146
+ Raises:
147
+ requests.exceptions.HTTPError: If the API request failed (e.g., group not found, user already member).
148
+ """
149
+ endpoint = f"{self.groups_url}/{group_name}/users/"
150
+ response = self.api_client.session.post(endpoint, json=users)
151
+ response.raise_for_status()
152
+ return response.json()
153
+
154
+ def remove_users_from_group(self, group_name: int, users: list[str]) -> bool:
155
+ """
156
+ Removes a user from a group.
157
+
158
+ Args:
159
+ group_name: The name of the group.
160
+ users: The users to remove.
161
+
162
+ Returns:
163
+ List of users that were removed.
164
+
165
+ Raises:
166
+ requests.exceptions.HTTPError: If the API request failed (e.g., group/user not found).
167
+ """
168
+ endpoint = f"{self.groups_url}/{group_name}/users"
169
+ response = self.api_client.session.delete(endpoint, json=users)
170
+ response.raise_for_status()
171
+ return response.json()
172
+
173
+ def is_member(self, group_name: str, user_name: str) -> bool:
174
+ """
175
+ Checks if a user is a member of a specific group.
176
+
177
+ Args:
178
+ group_name: The ID of the group.
179
+ user_name: The ID of the user.
180
+
181
+ Returns:
182
+ True if the user is a member, False otherwise.
183
+ """
184
+ endpoint = f"{self.groups_url}/{group_name}/users/"
185
+ response = self.api_client.session.get(endpoint)
186
+ username = None
187
+ response.raise_for_status() # Raises HTTPError for 4xx/5xx
188
+ for user in response.json():
189
+ username = user["username"]
190
+ return user_name == username
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import json
16
+ from typing import Any
16
17
 
17
18
  from openrelik_api_client.api_client import APIClient
18
19
 
@@ -24,7 +25,8 @@ class WorkflowsAPI:
24
25
  self.folders_url = f"{self.api_client.base_url}/folders"
25
26
 
26
27
  def create_workflow(
27
- self, folder_id: int, file_ids: list, template_id: int = None) -> int | None:
28
+ self, folder_id: int, file_ids: list, template_id: int = None
29
+ ) -> int | None:
28
30
  """Creates a new workflow.
29
31
 
30
32
  Args:
@@ -40,15 +42,18 @@ class WorkflowsAPI:
40
42
  """
41
43
  workflow_id = None
42
44
  endpoint = f"{self.folders_url}/{folder_id}/workflows/"
43
- data = {"folder_id": folder_id,
44
- "file_ids": file_ids, "template_id": template_id}
45
+ data = {
46
+ "folder_id": folder_id,
47
+ "file_ids": file_ids,
48
+ "template_id": template_id,
49
+ }
45
50
  response = self.api_client.session.post(endpoint, json=data)
46
51
  response.raise_for_status()
47
52
  if response.status_code == 200:
48
53
  workflow_id = response.json().get("id")
49
54
  return workflow_id
50
55
 
51
- def get_workflow(self, folder_id: int, workflow_id: int):
56
+ def get_workflow(self, folder_id: int, workflow_id: int) -> dict[str, Any]:
52
57
  """Retrieves a workflow by ID.
53
58
 
54
59
  Args:
@@ -67,7 +72,9 @@ class WorkflowsAPI:
67
72
  if response.status_code == 200:
68
73
  return response.json()
69
74
 
70
- def update_workflow(self, folder_id: int, workflow_id: int, workflow_data: dict):
75
+ def update_workflow(
76
+ self, folder_id: int, workflow_id: int, workflow_data: dict
77
+ ) -> dict[str, Any] | None:
71
78
  """Updates an existing workflow.
72
79
 
73
80
  Args:
@@ -107,7 +114,7 @@ class WorkflowsAPI:
107
114
  response.raise_for_status()
108
115
  return response.status_code == 204
109
116
 
110
- def run_workflow(self, folder_id: int, workflow_id: int):
117
+ def run_workflow(self, folder_id: int, workflow_id: int) -> dict[str, Any] | None:
111
118
  """Runs an existing workflow.
112
119
 
113
120
  Args:
@@ -122,11 +129,10 @@ class WorkflowsAPI:
122
129
  HTTPError: If the API request failed.
123
130
  """
124
131
  workflow = None
125
- endpoint = (
126
- f"{self.folders_url}/{folder_id}/workflows/{workflow_id}/run/")
132
+ endpoint = f"{self.folders_url}/{folder_id}/workflows/{workflow_id}/run/"
127
133
  workflow = self.get_workflow(folder_id, workflow_id)
128
- spec = json.loads(workflow.get('spec_json'))
129
- data = {'workflow_spec': spec}
134
+ spec = json.loads(workflow.get("spec_json"))
135
+ data = {"workflow_spec": spec}
130
136
  response = self.api_client.session.post(endpoint, json=data)
131
137
  response.raise_for_status()
132
138
  if response.status_code == 200:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "openrelik-api-client"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "API client that automatically handles token refresh"
5
5
  authors = ["Johan Berggren <jberggren@gmail.com>"]
6
6
  readme = "README.md"
@@ -1,108 +0,0 @@
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
- from typing import Any
16
-
17
- from openrelik_api_client.api_client import APIClient
18
-
19
-
20
- class FoldersAPI:
21
-
22
- def __init__(self, api_client: APIClient):
23
- super().__init__()
24
- self.api_client = api_client
25
-
26
- def create_root_folder(self, display_name: str) -> int | None:
27
- """Create a root folder.
28
-
29
- Args:
30
- display_name (str): Folder display name.
31
-
32
- Returns:
33
- int: Folder ID for the new root folder, or None otherwise.
34
-
35
- Raises:
36
- HTTPError: If the API request failed.
37
- """
38
- folder_id = None
39
- endpoint = f"{self.api_client.base_url}/folders/"
40
- params = {"display_name": display_name}
41
- response = self.api_client.session.post(endpoint, json=params)
42
- response.raise_for_status()
43
- if response.status_code == 201:
44
- folder_id = response.json().get('id')
45
- return folder_id
46
-
47
- def create_subfolder(
48
- self, folder_id: int, display_name: str) -> int | None:
49
- """Create a subfolder within the given folder ID.
50
-
51
- Args:
52
- folder_id: The ID of the parent folder.
53
- display_name: The name of the subfolder to check.
54
-
55
- Returns:
56
- int: Folder ID for the new root folder, or None.
57
-
58
- Raises:
59
- HTTPError: If the API request failed.
60
- """
61
- folder_id = None
62
- endpoint = f"{self.api_client.base_url}/folders/{folder_id}/folders"
63
- data = {"display_name": display_name}
64
- response = self.api_client.session.post(endpoint, json=data)
65
- response.raise_for_status()
66
- if response.status_code == 201:
67
- folder_id = response.json().get("id")
68
- return folder_id
69
-
70
- def folder_exists(self, folder_id: int) -> bool:
71
- """Checks if a folder with the given ID exists.
72
-
73
- Args:
74
- folder_id: The ID of the folder to check.
75
-
76
- Returns:
77
- True if the folder exists, False otherwise.
78
-
79
- Raises:
80
- HTTPError: If the API request failed.
81
- """
82
- endpoint = f"{self.api_client.base_url}/folders/{folder_id}"
83
- response = self.api_client.session.get(endpoint)
84
- response.raise_for_status()
85
- return response.status_code == 200
86
-
87
- def update_folder(
88
- self, folder_id: int, folder_data: dict[str, Any]
89
- ):
90
- """Updates an existing folder.
91
-
92
- Args:
93
- folder_id: The ID of the folder to update.
94
- folder_data: The updated folder data.
95
-
96
- Returns:
97
- The updated folder data, or None.
98
-
99
- Raises:
100
- HTTPError: If the API request failed.
101
- """
102
- endpoint = f"{self.api_client.base_url}/folders/{folder_id}"
103
- response = self.api_client.session.patch(
104
- endpoint,
105
- json=folder_data
106
- )
107
- response.raise_for_status()
108
- return response.json()