das-cli 1.0.6__py3-none-any.whl → 1.2.5__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.
- das/ai/plugins/dasai.py +11 -2
- das/cli.py +1234 -1110
- das/common/api.py +20 -0
- das/common/config.py +6 -0
- das/managers/digital_objects_manager.py +84 -0
- das/managers/download_manager.py +43 -1
- das/managers/entries_manager.py +37 -1
- das/managers/search_manager.py +17 -2
- das/services/digital_objects.py +142 -0
- das/services/downloads.py +101 -84
- das/services/entries.py +29 -3
- das_cli-1.2.5.dist-info/METADATA +1076 -0
- das_cli-1.2.5.dist-info/RECORD +32 -0
- {das_cli-1.0.6.dist-info → das_cli-1.2.5.dist-info}/WHEEL +1 -1
- das_cli-1.0.6.dist-info/METADATA +0 -408
- das_cli-1.0.6.dist-info/RECORD +0 -30
- {das_cli-1.0.6.dist-info → das_cli-1.2.5.dist-info}/entry_points.txt +0 -0
- {das_cli-1.0.6.dist-info → das_cli-1.2.5.dist-info}/licenses/LICENSE +0 -0
- {das_cli-1.0.6.dist-info → das_cli-1.2.5.dist-info}/top_level.txt +0 -0
das/common/api.py
CHANGED
|
@@ -27,6 +27,26 @@ def get_data(url, headers=None, params=None):
|
|
|
27
27
|
print(f"Error fetching API data: {e}")
|
|
28
28
|
return {"error": str(e)}
|
|
29
29
|
|
|
30
|
+
def get_binary_response(url, headers=None, params=None, stream=True):
|
|
31
|
+
"""
|
|
32
|
+
Perform a GET request expected to return binary content.
|
|
33
|
+
|
|
34
|
+
Returns the raw requests.Response so callers can inspect headers
|
|
35
|
+
and stream content to disk.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
response = requests.get(
|
|
39
|
+
url,
|
|
40
|
+
headers=headers,
|
|
41
|
+
params=params,
|
|
42
|
+
verify=load_verify_ssl(),
|
|
43
|
+
stream=stream,
|
|
44
|
+
)
|
|
45
|
+
response.raise_for_status()
|
|
46
|
+
return response
|
|
47
|
+
except requests.RequestException as e:
|
|
48
|
+
return {"error": str(e)}
|
|
49
|
+
|
|
30
50
|
def post_data(url, headers=None, data=None):
|
|
31
51
|
"""
|
|
32
52
|
Send data to a REST API endpoint.
|
das/common/config.py
CHANGED
|
@@ -81,12 +81,18 @@ def load_verify_ssl() -> bool:
|
|
|
81
81
|
|
|
82
82
|
if os.getenv("VERIFY_SSL") is not None:
|
|
83
83
|
VERIFY_SSL = os.getenv("VERIFY_SSL") == "True"
|
|
84
|
+
if not VERIFY_SSL:
|
|
85
|
+
print("SSL certificate verification is disabled")
|
|
84
86
|
return VERIFY_SSL
|
|
85
87
|
|
|
86
88
|
verify = config.get("verify_ssl")
|
|
87
89
|
if verify is not None:
|
|
88
90
|
VERIFY_SSL = verify
|
|
91
|
+
if not VERIFY_SSL:
|
|
92
|
+
print("SSL certificate verification is disabled")
|
|
89
93
|
return verify
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError("SSL certificate verification is not set")
|
|
90
96
|
except Exception:
|
|
91
97
|
pass
|
|
92
98
|
return VERIFY_SSL
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from das.common.config import load_api_url
|
|
4
|
+
from das.services.search import SearchService
|
|
5
|
+
from das.services.entries import EntriesService
|
|
6
|
+
from das.services.digital_objects import DigitalObjectsService
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
9
|
+
|
|
10
|
+
class DigitalObjectsManager:
|
|
11
|
+
"""Manager for digital objects."""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
base_url = load_api_url()
|
|
15
|
+
if base_url is None or base_url == "":
|
|
16
|
+
raise ValueError(f"Base URL is required - {self.__class__.__name__}")
|
|
17
|
+
|
|
18
|
+
self.__attribute_id_digital_object_type = 5;
|
|
19
|
+
self.digital_objects_service = DigitalObjectsService(base_url)
|
|
20
|
+
self.entry_service = EntriesService(base_url)
|
|
21
|
+
self.search_service = SearchService(base_url)
|
|
22
|
+
|
|
23
|
+
def link_existing_digital_objects(
|
|
24
|
+
self, entry_code: str, digital_object_code_list: list[str], is_unlink: bool = False
|
|
25
|
+
) -> bool:
|
|
26
|
+
"""Attach or detach (unlink) digital objects to an entry using codes."""
|
|
27
|
+
entry_response = self.entry_service.get_entry(entry_code)
|
|
28
|
+
|
|
29
|
+
if entry_response is None:
|
|
30
|
+
raise ValueError(f"Entry with code '{entry_code}' not found")
|
|
31
|
+
|
|
32
|
+
entry_payload = entry_response.get("entry")
|
|
33
|
+
if entry_payload is None:
|
|
34
|
+
raise ValueError(f"Entry with code '{entry_code}' not found")
|
|
35
|
+
|
|
36
|
+
digital_object_id_list: list[str] = []
|
|
37
|
+
|
|
38
|
+
for code in digital_object_code_list:
|
|
39
|
+
do_response = self.entry_service.get_entry(code)
|
|
40
|
+
do_entry = do_response.get("entry") if do_response else None
|
|
41
|
+
if do_entry is None:
|
|
42
|
+
raise ValueError(f"Digital object with code '{code}' not found")
|
|
43
|
+
digital_object_id_list.append(do_entry.get("id"))
|
|
44
|
+
|
|
45
|
+
result = self.digital_objects_service.link_existing_digital_objects(
|
|
46
|
+
attribute_id=entry_response.get("attributeId"),
|
|
47
|
+
entry_id=entry_payload.get("id"),
|
|
48
|
+
digital_object_id_list=digital_object_id_list,
|
|
49
|
+
is_unlink=is_unlink,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return result
|
|
53
|
+
|
|
54
|
+
def upload_digital_object(self, entry_code: str, file_description: str, digital_object_type: str, file_path: str):
|
|
55
|
+
"""Upload a digital object to the digital object service."""
|
|
56
|
+
response = self.search_service.search_entries(
|
|
57
|
+
queryString=f"displayname({digital_object_type})",
|
|
58
|
+
attributeId=self.__attribute_id_digital_object_type,
|
|
59
|
+
maxResultCount=1,
|
|
60
|
+
skipCount=0
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
entry_response = self.entry_service.get_entry(entry_code)
|
|
64
|
+
if entry_response is None:
|
|
65
|
+
raise ValueError(f"Entry with code '{entry_code}' not found")
|
|
66
|
+
|
|
67
|
+
if response.get('totalCount', 0) == 0:
|
|
68
|
+
raise ValueError(f"Digital object type '{digital_object_type}' not found")
|
|
69
|
+
|
|
70
|
+
digital_object_type_id = response.get('items', [])[0].get('entry').get('id')
|
|
71
|
+
digital_object_id = self.digital_objects_service.upload_digital_object(file_description, digital_object_type_id, file_path)
|
|
72
|
+
|
|
73
|
+
self.digital_objects_service.link_existing_digital_objects(
|
|
74
|
+
attribute_id=entry_response.get('attributeId'),
|
|
75
|
+
entry_id=entry_response.get('entry').get('id'),
|
|
76
|
+
digital_object_id_list=[digital_object_id]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return digital_object_id
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
digital_objects_manager = DigitalObjectsManager()
|
|
84
|
+
digital_objects_manager.upload_digital_object(entry_code="zb.b.f7", file_description="test", digital_object_type="Dataset", file_path="my_new_file.txt")
|
das/managers/download_manager.py
CHANGED
|
@@ -90,4 +90,46 @@ class DownloadManager:
|
|
|
90
90
|
|
|
91
91
|
def get_my_requests(self):
|
|
92
92
|
"""Get all download requests for the current user."""
|
|
93
|
-
return self.download_request_service.get_my_requests()
|
|
93
|
+
return self.download_request_service.get_my_requests()
|
|
94
|
+
|
|
95
|
+
def download_files(self, request_id: str):
|
|
96
|
+
"""Return streaming response for files of a download request."""
|
|
97
|
+
return self.download_request_service.download_files(request_id)
|
|
98
|
+
|
|
99
|
+
def save_download(self, request_id: str, output_path: str, overwrite: bool = False) -> str:
|
|
100
|
+
"""
|
|
101
|
+
Download and save the request bundle to disk.
|
|
102
|
+
|
|
103
|
+
Returns the path to the saved file.
|
|
104
|
+
"""
|
|
105
|
+
import os
|
|
106
|
+
|
|
107
|
+
resp = self.download_files(request_id)
|
|
108
|
+
# If an error structure was returned from lower layer
|
|
109
|
+
if isinstance(resp, dict) and resp.get('error'):
|
|
110
|
+
raise ValueError(resp['error'])
|
|
111
|
+
|
|
112
|
+
# Determine filename from headers if available
|
|
113
|
+
filename = f"download_{request_id}.zip"
|
|
114
|
+
try:
|
|
115
|
+
cd = resp.headers.get('Content-Disposition') if hasattr(resp, 'headers') else None
|
|
116
|
+
if cd and 'filename=' in cd:
|
|
117
|
+
filename = cd.split('filename=')[-1].strip('"')
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
# If output_path is a directory, join with filename
|
|
122
|
+
target_path = output_path
|
|
123
|
+
if os.path.isdir(output_path) or output_path.endswith(os.path.sep):
|
|
124
|
+
target_path = os.path.join(output_path, filename)
|
|
125
|
+
|
|
126
|
+
if os.path.exists(target_path) and not overwrite:
|
|
127
|
+
raise FileExistsError(f"File already exists: {target_path}. Use overwrite to replace.")
|
|
128
|
+
|
|
129
|
+
# Stream to disk
|
|
130
|
+
with open(target_path, 'wb') as f:
|
|
131
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
132
|
+
if chunk:
|
|
133
|
+
f.write(chunk)
|
|
134
|
+
|
|
135
|
+
return target_path
|
das/managers/entries_manager.py
CHANGED
|
@@ -23,6 +23,23 @@ class EntryManager:
|
|
|
23
23
|
self.attribute_service = AttributesService(base_url)
|
|
24
24
|
self.user_service = UsersService(base_url)
|
|
25
25
|
|
|
26
|
+
def delete(self, id: str = None, code: str = None) -> bool:
|
|
27
|
+
"""Delete an entry by its id or code."""
|
|
28
|
+
if not id and not code:
|
|
29
|
+
raise ValueError("Entry ID or code is required")
|
|
30
|
+
if id:
|
|
31
|
+
if self.entry_service.delete_by_id(id=id) is True:
|
|
32
|
+
return True
|
|
33
|
+
else:
|
|
34
|
+
return False
|
|
35
|
+
elif code:
|
|
36
|
+
if self.entry_service.delete(code=code) is True:
|
|
37
|
+
return True
|
|
38
|
+
else:
|
|
39
|
+
return False
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError("Entry ID or code is required")
|
|
42
|
+
|
|
26
43
|
def get_entry(self, entry_id: str):
|
|
27
44
|
"""Get entry details by ID"""
|
|
28
45
|
if not entry_id:
|
|
@@ -222,6 +239,9 @@ class EntryManager:
|
|
|
222
239
|
raise ValueError(f"Invalid existing entry response: {existing_entry_response}")
|
|
223
240
|
|
|
224
241
|
attribute_id = existing_entry_response.get("attributeId")
|
|
242
|
+
|
|
243
|
+
# make all keys lowercase
|
|
244
|
+
entry = {k.lower(): v for k, v in entry.items()}
|
|
225
245
|
|
|
226
246
|
if not attribute_id:
|
|
227
247
|
raise ValueError("Attribute ID is missing in the existing entry")
|
|
@@ -244,6 +264,10 @@ class EntryManager:
|
|
|
244
264
|
|
|
245
265
|
if field_name in entry:
|
|
246
266
|
updated_entry[column_name] = self.__get_value(field, entry[field_name])
|
|
267
|
+
elif column_name in entry:
|
|
268
|
+
updated_entry[column_name] = self.__get_value(field, entry[column_name])
|
|
269
|
+
else:
|
|
270
|
+
updated_entry[column_name] = None
|
|
247
271
|
|
|
248
272
|
return self.entry_service.update(attribute_id=attribute_id, entry=updated_entry)
|
|
249
273
|
|
|
@@ -276,7 +300,7 @@ class EntryManager:
|
|
|
276
300
|
if (datasource is not None and isinstance(datasource, dict) and "attributeid" in datasource):
|
|
277
301
|
attribute_id = datasource.get("attributeid")
|
|
278
302
|
except json.JSONDecodeError:
|
|
279
|
-
raise ValueError(f"Invalid customdata JSON: {field.get('customdata')}")
|
|
303
|
+
raise ValueError(f"Invalid customdata JSON: {field.get('customdata')}")
|
|
280
304
|
|
|
281
305
|
search_params = {
|
|
282
306
|
"attributeId": attribute_id,
|
|
@@ -285,6 +309,14 @@ class EntryManager:
|
|
|
285
309
|
"skipCount": 0
|
|
286
310
|
}
|
|
287
311
|
|
|
312
|
+
# checks if source is GUID, if so, than we search by id
|
|
313
|
+
if self.is_guid(source):
|
|
314
|
+
search_params['queryString'] = f"id({source});"
|
|
315
|
+
search_response = self.search_service.search_entries(**search_params)
|
|
316
|
+
else:
|
|
317
|
+
search_params['queryString'] = f"displayname({source});"
|
|
318
|
+
search_response = self.search_service.search_entries(**search_params)
|
|
319
|
+
|
|
288
320
|
search_response = self.search_service.search_entries(**search_params)
|
|
289
321
|
|
|
290
322
|
if search_response.get('totalCount', 0) == 0:
|
|
@@ -303,6 +335,10 @@ class EntryManager:
|
|
|
303
335
|
return json.dumps([result])
|
|
304
336
|
else:
|
|
305
337
|
return source
|
|
338
|
+
|
|
339
|
+
def is_guid(self, source: str) -> bool:
|
|
340
|
+
"""Helper method to check if a string is a GUID."""
|
|
341
|
+
return len(source) == 36 and source.count('-') == 4
|
|
306
342
|
|
|
307
343
|
def __get_field_value(self, entry_raw, field):
|
|
308
344
|
"""Helper method to safely get field value from entry_raw."""
|
das/managers/search_manager.py
CHANGED
|
@@ -3,7 +3,6 @@ from das.services.attributes import AttributesService
|
|
|
3
3
|
from das.services.entry_fields import EntryFieldsService
|
|
4
4
|
from das.services.search import SearchService
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
class SearchManager:
|
|
8
7
|
def __init__(self):
|
|
9
8
|
base_url = load_api_url()
|
|
@@ -59,6 +58,22 @@ class SearchManager:
|
|
|
59
58
|
"sorting": self.__convert_sorting(entry_fields, sort_by, sort_order)
|
|
60
59
|
}
|
|
61
60
|
results = self.search_service.search_entries(**search_params)
|
|
62
|
-
|
|
61
|
+
|
|
62
|
+
# Build user-friendly items list while preserving totalCount
|
|
63
|
+
friendly_items = []
|
|
64
|
+
for result in results.get('items', []):
|
|
65
|
+
entry = result.get('entry', {}) if isinstance(result, dict) else {}
|
|
66
|
+
friendly_item = {}
|
|
67
|
+
for field in entry_fields:
|
|
68
|
+
display_name = field.get('displayName')
|
|
69
|
+
column_name = field.get('column')
|
|
70
|
+
if display_name and column_name:
|
|
71
|
+
friendly_item[display_name] = entry.get(column_name)
|
|
72
|
+
friendly_items.append(friendly_item)
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
'items': friendly_items,
|
|
76
|
+
'totalCount': results.get('totalCount', len(friendly_items))
|
|
77
|
+
}
|
|
63
78
|
except Exception as e:
|
|
64
79
|
raise ValueError(f"Search failed: {str(e)}")
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from math import ceil
|
|
4
|
+
from os.path import exists
|
|
5
|
+
import json
|
|
6
|
+
from base64 import b64encode
|
|
7
|
+
from das.common.api import post_data
|
|
8
|
+
from das.common.config import load_token, load_verify_ssl
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import math
|
|
11
|
+
import uuid
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
CHUNK_SIZE = 1000000 # 1MB
|
|
15
|
+
class DigitalObjectsService:
|
|
16
|
+
def __init__(self, base_url):
|
|
17
|
+
self.base_url = f"{base_url}/api/services/app/DigitalObject"
|
|
18
|
+
# Common possible upload endpoints observed across deployments
|
|
19
|
+
self.upload_digital_object_url = f"{base_url}/File/UploadDigitalObject"
|
|
20
|
+
|
|
21
|
+
def link_existing_digital_objects(self, attribute_id: int, entry_id: str, digital_object_id_list: list[str], is_unlink: bool = False):
|
|
22
|
+
"""Link existing digital objects to an entry."""
|
|
23
|
+
token = load_token()
|
|
24
|
+
|
|
25
|
+
if token is None or token == "":
|
|
26
|
+
raise ValueError("Authorization token is required")
|
|
27
|
+
|
|
28
|
+
headers = {
|
|
29
|
+
"Authorization": f"Bearer {token}",
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
payload = {
|
|
34
|
+
"attributeId": attribute_id,
|
|
35
|
+
"attributeValueId": entry_id,
|
|
36
|
+
"digitalObjects": [],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for digital_object_id in digital_object_id_list:
|
|
40
|
+
payload["digitalObjects"].append(
|
|
41
|
+
{
|
|
42
|
+
"attributeId": attribute_id,
|
|
43
|
+
"attributeValueId": entry_id,
|
|
44
|
+
"digitalObjectId": digital_object_id,
|
|
45
|
+
"isDeleted": is_unlink,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
response = post_data(
|
|
50
|
+
f"{self.base_url}/LinkExistingDigitalObject", data=payload, headers=headers
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return response.get("success")
|
|
54
|
+
|
|
55
|
+
# This is our chunk reader. This is what gets the next chunk of data ready to send.
|
|
56
|
+
def __read_in_chunks(self, file_object, chunk_size):
|
|
57
|
+
while True:
|
|
58
|
+
data = file_object.read(chunk_size)
|
|
59
|
+
if not data:
|
|
60
|
+
break
|
|
61
|
+
yield data
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def upload_digital_object(self, file_description: str, digital_object_type_id: str, file_path: str):
|
|
65
|
+
|
|
66
|
+
if not exists(file_path):
|
|
67
|
+
raise ValueError(f"File '{file_path}' does not exist")
|
|
68
|
+
|
|
69
|
+
head, tail = os.path.split(file_path)
|
|
70
|
+
|
|
71
|
+
metadata = {
|
|
72
|
+
"fileName": tail,
|
|
73
|
+
"fileSize": os.path.getsize(file_path),
|
|
74
|
+
"description": file_description,
|
|
75
|
+
"digitalObjectTypeId": digital_object_type_id,
|
|
76
|
+
"id": str(uuid.uuid4()).lower(),
|
|
77
|
+
"description": file_description,
|
|
78
|
+
"totalCount": ceil(os.path.getsize(file_path) / CHUNK_SIZE),
|
|
79
|
+
"index": 0,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
binary_file = open(file_path, "rb")
|
|
83
|
+
index = 0
|
|
84
|
+
offset = 0
|
|
85
|
+
digital_object_id = None
|
|
86
|
+
headers = {}
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
for chunk in self.__read_in_chunks(binary_file, CHUNK_SIZE):
|
|
90
|
+
offset = index + len(chunk)
|
|
91
|
+
headers['Content-Range'] = 'bytes %s-%s/%s' % (index, offset - 1, metadata.get('fileSize'))
|
|
92
|
+
index = offset
|
|
93
|
+
json_metadata = json.dumps(metadata)
|
|
94
|
+
base654_bytes = b64encode(json_metadata.encode('utf-8')).decode('ascii')
|
|
95
|
+
headers['metadata'] = base654_bytes
|
|
96
|
+
|
|
97
|
+
r = self.upload_file(chunk, metadata, headers)
|
|
98
|
+
|
|
99
|
+
if r.get('result', None) is None:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
digital_object_id = r.get('result').get('id')
|
|
103
|
+
metadata['index'] = index + 1
|
|
104
|
+
|
|
105
|
+
binary_file.close()
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
raise ValueError(f"Error uploading file '{file_path}': {str(e)}")
|
|
109
|
+
finally:
|
|
110
|
+
binary_file.close()
|
|
111
|
+
|
|
112
|
+
return digital_object_id
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def upload_file(self, file, body, headers):
|
|
117
|
+
"""Upload a file to the digital object service."""
|
|
118
|
+
token = load_token()
|
|
119
|
+
headers.update({
|
|
120
|
+
"Accept": "application/json",
|
|
121
|
+
"Authorization": f"Bearer {token}",
|
|
122
|
+
# Do NOT set Content-Type here when sending files; requests will set proper multipart boundary
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
files = {
|
|
126
|
+
"file": ("chunk", file, "application/octet-stream"),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
response = requests.post(self.upload_digital_object_url, headers=headers, files=files, verify=load_verify_ssl())
|
|
131
|
+
response.raise_for_status()
|
|
132
|
+
if response.status_code == 200:
|
|
133
|
+
return response.json()
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError(f"Error uploading file: {response.status_code} - {response.text}")
|
|
136
|
+
except requests.RequestException as e:
|
|
137
|
+
raise ValueError(f"Error uploading file: {str(e)}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
das/services/downloads.py
CHANGED
|
@@ -1,84 +1,101 @@
|
|
|
1
|
-
from das.common.api import post_data, get_data
|
|
2
|
-
from das.common.config import load_token
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class DownloadRequestService:
|
|
6
|
-
def __init__(self, base_url):
|
|
7
|
-
self.base_url = f"{base_url}/api/services/app/DownloadRequest"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
1
|
+
from das.common.api import post_data, get_data, get_binary_response
|
|
2
|
+
from das.common.config import load_token
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DownloadRequestService:
|
|
6
|
+
def __init__(self, base_url):
|
|
7
|
+
self.base_url = f"{base_url}/api/services/app/DownloadRequest"
|
|
8
|
+
self.download_files_url = f"{base_url}/File/DownloadRequestSet"
|
|
9
|
+
|
|
10
|
+
def create(self, request_data: list[dict]):
|
|
11
|
+
"""Create a new download request."""
|
|
12
|
+
token = load_token()
|
|
13
|
+
|
|
14
|
+
if (token is None or token == ""):
|
|
15
|
+
raise ValueError("Authorization token is required")
|
|
16
|
+
|
|
17
|
+
headers = {
|
|
18
|
+
"Authorization": f"Bearer {token}",
|
|
19
|
+
"Content-Type": "application/json"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
url = f"{self.base_url}/Create"
|
|
23
|
+
|
|
24
|
+
response = post_data(url, data=request_data, headers=headers)
|
|
25
|
+
|
|
26
|
+
if response.get('success') == True:
|
|
27
|
+
return response.get('result')
|
|
28
|
+
else:
|
|
29
|
+
raise ValueError(response.get('error'))
|
|
30
|
+
|
|
31
|
+
def delete(self, request_id: str):
|
|
32
|
+
"""Delete a download request by ID."""
|
|
33
|
+
|
|
34
|
+
#check if request_id is valid uuid
|
|
35
|
+
if not isinstance(request_id, str) or len(request_id) != 36:
|
|
36
|
+
raise ValueError("Invalid request ID")
|
|
37
|
+
|
|
38
|
+
token = load_token()
|
|
39
|
+
|
|
40
|
+
if (token is None or token == ""):
|
|
41
|
+
raise ValueError("Authorization token is required")
|
|
42
|
+
|
|
43
|
+
headers = {
|
|
44
|
+
"Authorization": f"Bearer {token}",
|
|
45
|
+
"Content-Type": "application/json"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
url = f"{self.base_url}/Delete?downloadRequestId={request_id}"
|
|
49
|
+
|
|
50
|
+
response = post_data(url, data={}, headers=headers)
|
|
51
|
+
|
|
52
|
+
if response.get('success') == True:
|
|
53
|
+
return response.get('result')
|
|
54
|
+
else:
|
|
55
|
+
raise ValueError(response.get('error'))
|
|
56
|
+
|
|
57
|
+
def get_my_requests(self):
|
|
58
|
+
"""Get all download requests for the current user."""
|
|
59
|
+
token = load_token()
|
|
60
|
+
|
|
61
|
+
if (token is None or token == ""):
|
|
62
|
+
raise ValueError("Authorization token is required")
|
|
63
|
+
|
|
64
|
+
headers = {
|
|
65
|
+
"Authorization": f"Bearer {token}"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
url = f"{self.base_url}/GetMyRequests"
|
|
69
|
+
response = get_data(url, headers=headers)
|
|
70
|
+
|
|
71
|
+
# Expected API response shape:
|
|
72
|
+
# { success: true, result: { totalCount: number, items: [...] }, ... }
|
|
73
|
+
if isinstance(response, dict) and response.get('success') is True:
|
|
74
|
+
return response.get('result')
|
|
75
|
+
# Some backends might already return the result without 'success'
|
|
76
|
+
if isinstance(response, dict) and 'result' in response and 'success' not in response:
|
|
77
|
+
return response.get('result')
|
|
78
|
+
# If the API directly returns the payload (result), pass it through
|
|
79
|
+
if isinstance(response, dict) and 'items' in response and 'totalCount' in response:
|
|
80
|
+
return response
|
|
81
|
+
# Otherwise raise a meaningful error
|
|
82
|
+
error_msg = None
|
|
83
|
+
if isinstance(response, dict):
|
|
84
|
+
error_msg = response.get('error') or response.get('message')
|
|
85
|
+
raise ValueError(error_msg or 'Failed to fetch download requests')
|
|
86
|
+
|
|
87
|
+
def download_files(self, request_id: str):
|
|
88
|
+
"""Return a streaming HTTP response for the download bundle of a request."""
|
|
89
|
+
token = load_token()
|
|
90
|
+
|
|
91
|
+
if (token is None or token == ""):
|
|
92
|
+
raise ValueError("Authorization token is required")
|
|
93
|
+
|
|
94
|
+
headers = {
|
|
95
|
+
"Authorization": f"Bearer {token}"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
url = f"{self.download_files_url}?requestId={request_id}"
|
|
99
|
+
|
|
100
|
+
response = get_binary_response(url, headers=headers, params=None, stream=True)
|
|
101
|
+
return response
|
das/services/entries.py
CHANGED
|
@@ -52,8 +52,34 @@ class EntriesService():
|
|
|
52
52
|
raise ValueError(f"API returned invalid JSON: {response.get('error')}\nResponse content: {raw_content}")
|
|
53
53
|
else:
|
|
54
54
|
raise ValueError(response.get('error') or "Unknown error occurred")
|
|
55
|
-
|
|
56
|
-
def
|
|
55
|
+
|
|
56
|
+
def delete_by_id(self, id: str) -> bool:
|
|
57
|
+
"""Delete an entry by its id."""
|
|
58
|
+
|
|
59
|
+
token = load_token()
|
|
60
|
+
|
|
61
|
+
if (token is None or token == ""):
|
|
62
|
+
raise ValueError("Authorization token is required")
|
|
63
|
+
|
|
64
|
+
headers = {
|
|
65
|
+
"Authorization": f"Bearer {token}"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
response_entry = self.get(id=id)
|
|
69
|
+
|
|
70
|
+
if response_entry is None:
|
|
71
|
+
raise ValueError(f"Entry with id {id} not found")
|
|
72
|
+
|
|
73
|
+
url = f"{self.base_url}/Delete?Id={response_entry.get('entry',{}).get('id')}&AttributeId={response_entry.get('attributeId')}"
|
|
74
|
+
|
|
75
|
+
response = delete_data(url, headers=headers)
|
|
76
|
+
|
|
77
|
+
if response.get('success') == True:
|
|
78
|
+
return True
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError(response.get('error'))
|
|
81
|
+
|
|
82
|
+
def delete(self, code: str) -> bool:
|
|
57
83
|
"""Delete an entry by its code."""
|
|
58
84
|
token = load_token()
|
|
59
85
|
|
|
@@ -74,7 +100,7 @@ class EntriesService():
|
|
|
74
100
|
response = delete_data(url, headers=headers)
|
|
75
101
|
|
|
76
102
|
if response.get('success') == True:
|
|
77
|
-
return
|
|
103
|
+
return True
|
|
78
104
|
else:
|
|
79
105
|
raise ValueError(response.get('error'))
|
|
80
106
|
|