das-cli 1.0.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.
- das/__init__.py +0 -0
- das/ai/plugins/dasai.py +50 -0
- das/ai/plugins/entries/entries_plugin.py +16 -0
- das/app.py +37 -0
- das/authentication/auth.py +43 -0
- das/authentication/secure_input.py +67 -0
- das/cli.py +1070 -0
- das/common/api.py +100 -0
- das/common/config.py +185 -0
- das/common/entry_fields_constants.py +3 -0
- das/common/enums.py +46 -0
- das/common/file_utils.py +203 -0
- das/managers/__init__.py +0 -0
- das/managers/download_manager.py +93 -0
- das/managers/entries_manager.py +433 -0
- das/managers/search_manager.py +64 -0
- das/services/attributes.py +81 -0
- das/services/cache.py +70 -0
- das/services/downloads.py +84 -0
- das/services/entries.py +132 -0
- das/services/entry_fields.py +33 -0
- das/services/hangfire.py +26 -0
- das/services/search.py +33 -0
- das_cli-1.0.0.dist-info/METADATA +408 -0
- das_cli-1.0.0.dist-info/RECORD +29 -0
- das_cli-1.0.0.dist-info/WHEEL +5 -0
- das_cli-1.0.0.dist-info/entry_points.txt +2 -0
- das_cli-1.0.0.dist-info/licenses/LICENSE +22 -0
- das_cli-1.0.0.dist-info/top_level.txt +1 -0
das/common/api.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
from das.common.config import VERIFY_SSL
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_data(url, headers=None, params=None):
|
|
7
|
+
"""
|
|
8
|
+
Fetch data from a REST API endpoint.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
url (str): The API endpoint URL.
|
|
12
|
+
headers (dict, optional): Headers to include in the request.
|
|
13
|
+
params (dict, optional): Query parameters for the request.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
dict: The JSON response from the API or an error message.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
response = requests.get(url, headers=headers, params=params, verify=VERIFY_SSL)
|
|
20
|
+
response.raise_for_status() # Raise an error for HTTP errors
|
|
21
|
+
try:
|
|
22
|
+
return response.json()
|
|
23
|
+
except json.JSONDecodeError as json_err:
|
|
24
|
+
print(f"Error decoding JSON response: {json_err}")
|
|
25
|
+
return {"error": f"Invalid JSON response: {json_err}", "raw_content": response.text[:200]}
|
|
26
|
+
except requests.RequestException as e:
|
|
27
|
+
print(f"Error fetching API data: {e}")
|
|
28
|
+
return {"error": str(e)}
|
|
29
|
+
|
|
30
|
+
def post_data(url, headers=None, data=None):
|
|
31
|
+
"""
|
|
32
|
+
Send data to a REST API endpoint.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
url (str): The API endpoint URL.
|
|
36
|
+
headers (dict, optional): Headers to include in the request.
|
|
37
|
+
data (dict, optional): The data to send in the request body.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
dict: The JSON response from the API or an error message.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
response = requests.post(url, headers=headers, json=data, verify=VERIFY_SSL)
|
|
44
|
+
response.raise_for_status() # Raise an error for HTTP errors
|
|
45
|
+
try:
|
|
46
|
+
return response.json()
|
|
47
|
+
except json.JSONDecodeError as json_err:
|
|
48
|
+
print(f"Error decoding JSON response: {json_err}")
|
|
49
|
+
return {"error": f"Invalid JSON response: {json_err}", "raw_content": response.text[:200]}
|
|
50
|
+
except requests.RequestException as e:
|
|
51
|
+
print(f"Error posting API data: {e}")
|
|
52
|
+
return {"error": str(e)}
|
|
53
|
+
|
|
54
|
+
def put_data(url, headers=None, data=None):
|
|
55
|
+
"""
|
|
56
|
+
Update data at a REST API endpoint.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
url (str): The API endpoint URL.
|
|
60
|
+
headers (dict, optional): Headers to include in the request.
|
|
61
|
+
data (dict, optional): The data to send in the request body.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
dict: The JSON response from the API or an error message.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
response = requests.put(url, headers=headers, json=data, verify=VERIFY_SSL)
|
|
68
|
+
response.raise_for_status() # Raise an error for HTTP errors
|
|
69
|
+
try:
|
|
70
|
+
return response.json()
|
|
71
|
+
except json.JSONDecodeError as json_err:
|
|
72
|
+
print(f"Error decoding JSON response: {json_err}")
|
|
73
|
+
return {"error": f"Invalid JSON response: {json_err}", "raw_content": response.text[:200]}
|
|
74
|
+
except requests.RequestException as e:
|
|
75
|
+
print(f"Error updating API data: {e}")
|
|
76
|
+
return {"error": str(e)}
|
|
77
|
+
|
|
78
|
+
def delete_data(url, headers=None, data=None):
|
|
79
|
+
"""
|
|
80
|
+
Delete data from a REST API endpoint.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
url (str): The API endpoint URL.
|
|
84
|
+
headers (dict, optional): Headers to include in the request.
|
|
85
|
+
data (dict, optional): The data to send in the request body.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
dict: The JSON response from the API or an error message.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
response = requests.delete(url, headers=headers, json=data, verify=VERIFY_SSL)
|
|
92
|
+
response.raise_for_status() # Raise an error for HTTP errors
|
|
93
|
+
try:
|
|
94
|
+
return response.json()
|
|
95
|
+
except json.JSONDecodeError as json_err:
|
|
96
|
+
print(f"Error decoding JSON response: {json_err}")
|
|
97
|
+
return {"error": f"Invalid JSON response: {json_err}", "raw_content": response.text[:200]}
|
|
98
|
+
except requests.RequestException as e:
|
|
99
|
+
print(f"Error deleting API data: {e}")
|
|
100
|
+
return {"error": str(e)}
|
das/common/config.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
# Set up logging
|
|
8
|
+
logging.basicConfig(
|
|
9
|
+
level=logging.WARNING, # Only show warnings and errors by default
|
|
10
|
+
format='%(message)s' # Simplified format for cleaner output
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# ---------- Config ----------
|
|
14
|
+
|
|
15
|
+
SERVICE_NAME = "Data-Archive-System"
|
|
16
|
+
DEFAULT_BASE_URL = "https://localhost:44301" # ← change to your API
|
|
17
|
+
VERIFY_SSL = True # Default is True for secure connections
|
|
18
|
+
TOKEN_FIELD = "accessToken"
|
|
19
|
+
|
|
20
|
+
def _config_dir() -> Path:
|
|
21
|
+
if os.name == "nt": # Windows
|
|
22
|
+
base = Path(os.getenv("APPDATA", Path.home()))
|
|
23
|
+
else: # macOS and Linux
|
|
24
|
+
base = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
25
|
+
d = Path(base) / SERVICE_NAME
|
|
26
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
return d
|
|
28
|
+
|
|
29
|
+
CONFIG_FILE = _config_dir() / "config.json"
|
|
30
|
+
|
|
31
|
+
def save_api_url(api_url: str) -> None:
|
|
32
|
+
config = {}
|
|
33
|
+
if CONFIG_FILE.exists():
|
|
34
|
+
try:
|
|
35
|
+
config = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
36
|
+
except Exception:
|
|
37
|
+
config = {}
|
|
38
|
+
config["api_url"] = api_url
|
|
39
|
+
CONFIG_FILE.write_text(json.dumps(config), encoding="utf-8")
|
|
40
|
+
|
|
41
|
+
def load_api_url() -> Optional[str]:
|
|
42
|
+
if CONFIG_FILE.exists():
|
|
43
|
+
try:
|
|
44
|
+
return json.loads(CONFIG_FILE.read_text(encoding="utf-8")).get("api_url")
|
|
45
|
+
except Exception:
|
|
46
|
+
return None
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
def save_verify_ssl(verify: bool) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Save the VERIFY_SSL setting to the config file.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
verify (bool): Whether to verify SSL certificates.
|
|
55
|
+
"""
|
|
56
|
+
global VERIFY_SSL
|
|
57
|
+
config = {}
|
|
58
|
+
if CONFIG_FILE.exists():
|
|
59
|
+
try:
|
|
60
|
+
config = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
61
|
+
except Exception:
|
|
62
|
+
config = {}
|
|
63
|
+
config["verify_ssl"] = verify
|
|
64
|
+
VERIFY_SSL = verify
|
|
65
|
+
CONFIG_FILE.write_text(json.dumps(config), encoding="utf-8")
|
|
66
|
+
|
|
67
|
+
def load_verify_ssl() -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Load the VERIFY_SSL setting from the config file.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
bool: The VERIFY_SSL setting, defaults to True if not found.
|
|
73
|
+
"""
|
|
74
|
+
global VERIFY_SSL
|
|
75
|
+
if CONFIG_FILE.exists():
|
|
76
|
+
try:
|
|
77
|
+
config = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
78
|
+
verify = config.get("verify_ssl")
|
|
79
|
+
if verify is not None:
|
|
80
|
+
VERIFY_SSL = verify
|
|
81
|
+
return verify
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
return VERIFY_SSL
|
|
85
|
+
|
|
86
|
+
def _config_dir() -> Path:
|
|
87
|
+
if os.name == "nt": # Windows
|
|
88
|
+
base = Path(os.getenv("APPDATA", Path.home()))
|
|
89
|
+
else: # macOS and Linux
|
|
90
|
+
base = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
91
|
+
d = Path(base) / SERVICE_NAME
|
|
92
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
return d
|
|
94
|
+
|
|
95
|
+
def _token_path() -> Path:
|
|
96
|
+
return _config_dir() / "token.json"
|
|
97
|
+
|
|
98
|
+
def _openai_key_path() -> Path:
|
|
99
|
+
return _config_dir() / "openai_key.json"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _keyring_available() -> bool:
|
|
103
|
+
try:
|
|
104
|
+
import keyring # noqa: F401
|
|
105
|
+
return True
|
|
106
|
+
except Exception:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
def save_token(token: str) -> None:
|
|
110
|
+
if _keyring_available():
|
|
111
|
+
import keyring
|
|
112
|
+
keyring.set_password(SERVICE_NAME, "default", token)
|
|
113
|
+
else:
|
|
114
|
+
p = _token_path()
|
|
115
|
+
p.write_text(json.dumps({"token": token}), encoding="utf-8")
|
|
116
|
+
if os.name != "nt":
|
|
117
|
+
os.chmod(p, 0o600)
|
|
118
|
+
|
|
119
|
+
def load_token() -> Optional[str]:
|
|
120
|
+
if _keyring_available():
|
|
121
|
+
import keyring
|
|
122
|
+
return keyring.get_password(SERVICE_NAME, "default")
|
|
123
|
+
p = _token_path()
|
|
124
|
+
if p.exists():
|
|
125
|
+
try:
|
|
126
|
+
return json.loads(p.read_text(encoding="utf-8")).get("token")
|
|
127
|
+
except Exception:
|
|
128
|
+
return None
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def clear_token() -> None:
|
|
132
|
+
if _keyring_available():
|
|
133
|
+
import keyring
|
|
134
|
+
try:
|
|
135
|
+
keyring.delete_password(SERVICE_NAME, "default")
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
try:
|
|
139
|
+
_token_path().unlink()
|
|
140
|
+
except FileNotFoundError:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
# ---------- OpenAI API Key management ----------
|
|
144
|
+
|
|
145
|
+
def save_openai_api_key(api_key: str) -> None:
|
|
146
|
+
"""Persist the OpenAI API key securely (keyring if available, else protected file)."""
|
|
147
|
+
if _keyring_available():
|
|
148
|
+
import keyring
|
|
149
|
+
keyring.set_password(SERVICE_NAME, "openai_api_key", api_key)
|
|
150
|
+
else:
|
|
151
|
+
p = _openai_key_path()
|
|
152
|
+
p.write_text(json.dumps({"api_key": api_key}), encoding="utf-8")
|
|
153
|
+
if os.name != "nt":
|
|
154
|
+
os.chmod(p, 0o600)
|
|
155
|
+
|
|
156
|
+
def load_openai_api_key() -> Optional[str]:
|
|
157
|
+
"""Retrieve the stored OpenAI API key, if present."""
|
|
158
|
+
if _keyring_available():
|
|
159
|
+
import keyring
|
|
160
|
+
val = keyring.get_password(SERVICE_NAME, "openai_api_key")
|
|
161
|
+
if val:
|
|
162
|
+
return val
|
|
163
|
+
p = _openai_key_path()
|
|
164
|
+
if p.exists():
|
|
165
|
+
try:
|
|
166
|
+
return json.loads(p.read_text(encoding="utf-8")).get("api_key")
|
|
167
|
+
except Exception:
|
|
168
|
+
return None
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
def clear_openai_api_key() -> None:
|
|
172
|
+
"""Remove any stored OpenAI API key from secure storage and file fallback."""
|
|
173
|
+
if _keyring_available():
|
|
174
|
+
import keyring
|
|
175
|
+
try:
|
|
176
|
+
keyring.delete_password(SERVICE_NAME, "openai_api_key")
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
try:
|
|
180
|
+
_openai_key_path().unlink()
|
|
181
|
+
except FileNotFoundError:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
# Initialize VERIFY_SSL from config file if available
|
|
185
|
+
load_verify_ssl()
|
das/common/enums.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
class DownloadRequestStatus(IntEnum):
|
|
4
|
+
NONE = 0
|
|
5
|
+
# When a user starts their download request.
|
|
6
|
+
ENQUEUED = 1
|
|
7
|
+
# Download approval requested from the digital object's owner.
|
|
8
|
+
APPROVAL_REQUESTED = 2
|
|
9
|
+
# Waiting for approval from the owner to download the requested files.
|
|
10
|
+
WAITING_FOR_APPROVAL = 3
|
|
11
|
+
# All files in the current request were approved (if applicable).
|
|
12
|
+
APPROVED = 4
|
|
13
|
+
# The download request was not authorized by the owner(s).
|
|
14
|
+
DECLINED = 5
|
|
15
|
+
# Approved requests will be prepared to be delivered.
|
|
16
|
+
HANDLE_FILES_TO_BE_DELIVERED = 6
|
|
17
|
+
# Status while the set of files are being compacted.
|
|
18
|
+
COMPACTING_BUNDLE = 7
|
|
19
|
+
# The download request is completed and ready to be downloaded.
|
|
20
|
+
COMPLETED = 8
|
|
21
|
+
# Incomplete request: not all files are available to download.
|
|
22
|
+
INCOMPLETE = 9
|
|
23
|
+
# Request is invalid or cannot be processed due to missing/invalid data.
|
|
24
|
+
FAILED = 10
|
|
25
|
+
|
|
26
|
+
class DownloadRequestItemStatus(IntEnum):
|
|
27
|
+
# Digital Object download request was enqueued.
|
|
28
|
+
ENQUEUED = 1
|
|
29
|
+
# Download approval requested from the digital object's owner.
|
|
30
|
+
APPROVAL_REQUESTED = 2
|
|
31
|
+
# Waiting for approval by the digital object owner.
|
|
32
|
+
WAITING_FOR_APPROVAL = 3
|
|
33
|
+
# Download of the digital object was approved by its owner.
|
|
34
|
+
APPROVED = 4
|
|
35
|
+
# Download of the digital object was denied by its owner.
|
|
36
|
+
DECLINED = 5
|
|
37
|
+
# Notification of decline has been sent.
|
|
38
|
+
DECLINED_NOTIFICATION_SENT = 6
|
|
39
|
+
# Waiting to be downloaded.
|
|
40
|
+
WAITING_TO_BE_DOWNLOADED = 7
|
|
41
|
+
# In process.
|
|
42
|
+
IN_PROCESS = 8
|
|
43
|
+
# File is ready to be delivered.
|
|
44
|
+
AVAILABLE_TO_DOWNLOAD = 9
|
|
45
|
+
# Internal error occurred.
|
|
46
|
+
ERROR = 10
|
das/common/file_utils.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File utility functions for importing data from various formats
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import csv
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, List, Union
|
|
10
|
+
|
|
11
|
+
def load_json_file(file_path: str) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
|
|
12
|
+
"""
|
|
13
|
+
Load data from a JSON file.
|
|
14
|
+
Can load either a single entry (dictionary) or multiple entries (list of dictionaries).
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
file_path (str): Path to the JSON file
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Union[List[Dict[str, Any]], Dict[str, Any]]: The loaded JSON data
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: If the file doesn't exist or isn't a valid JSON file
|
|
24
|
+
"""
|
|
25
|
+
file_path = Path(file_path)
|
|
26
|
+
|
|
27
|
+
if not file_path.exists():
|
|
28
|
+
raise ValueError(f"File not found: {file_path}")
|
|
29
|
+
|
|
30
|
+
if file_path.suffix.lower() != '.json':
|
|
31
|
+
raise ValueError(f"Not a JSON file: {file_path}")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
35
|
+
data = json.load(f)
|
|
36
|
+
return data
|
|
37
|
+
except json.JSONDecodeError as e:
|
|
38
|
+
raise ValueError(f"Invalid JSON file: {e}")
|
|
39
|
+
|
|
40
|
+
def load_csv_file(file_path: str) -> List[Dict[str, Any]]:
|
|
41
|
+
"""
|
|
42
|
+
Load data from a CSV file.
|
|
43
|
+
Assumes the first row contains headers and subsequent rows contain values.
|
|
44
|
+
Creates a list of dictionaries, one for each row in the CSV file.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
file_path (str): Path to the CSV file
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List[Dict[str, Any]]: A list of dictionaries, each representing one row from the CSV
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ValueError: If the file doesn't exist or isn't a valid CSV file
|
|
54
|
+
"""
|
|
55
|
+
file_path = Path(file_path)
|
|
56
|
+
|
|
57
|
+
if not file_path.exists():
|
|
58
|
+
raise ValueError(f"File not found: {file_path}")
|
|
59
|
+
|
|
60
|
+
if file_path.suffix.lower() != '.csv':
|
|
61
|
+
raise ValueError(f"Not a CSV file: {file_path}")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
with open(file_path, 'r', encoding='utf-8', newline='') as f:
|
|
65
|
+
reader = csv.reader(f)
|
|
66
|
+
headers = next(reader, None)
|
|
67
|
+
|
|
68
|
+
if not headers:
|
|
69
|
+
raise ValueError("CSV file is empty or has no headers")
|
|
70
|
+
|
|
71
|
+
result = []
|
|
72
|
+
for row in reader:
|
|
73
|
+
if not row: # Skip empty rows
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# Create a dictionary for this row
|
|
77
|
+
entry = {}
|
|
78
|
+
for i, header in enumerate(headers):
|
|
79
|
+
if i < len(row):
|
|
80
|
+
entry[header] = row[i]
|
|
81
|
+
else:
|
|
82
|
+
entry[header] = "" # Empty value for missing columns
|
|
83
|
+
|
|
84
|
+
result.append(entry)
|
|
85
|
+
|
|
86
|
+
if not result:
|
|
87
|
+
raise ValueError("CSV file has headers but no data rows")
|
|
88
|
+
|
|
89
|
+
return result
|
|
90
|
+
except csv.Error as e:
|
|
91
|
+
raise ValueError(f"CSV parsing error: {e}")
|
|
92
|
+
except UnicodeDecodeError:
|
|
93
|
+
raise ValueError("Unable to decode CSV file, ensure it's saved with UTF-8 encoding")
|
|
94
|
+
|
|
95
|
+
def load_excel_file(file_path: str) -> List[Dict[str, Any]]:
|
|
96
|
+
"""
|
|
97
|
+
Load data from an Excel file.
|
|
98
|
+
Assumes the first row contains headers and subsequent rows contain values.
|
|
99
|
+
Creates a list of dictionaries, one for each row in the Excel file.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
file_path (str): Path to the Excel file
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List[Dict[str, Any]]: A list of dictionaries, each representing one row from the Excel file
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
ValueError: If the file doesn't exist, isn't a valid Excel file, or pandas is not installed
|
|
109
|
+
"""
|
|
110
|
+
file_path = Path(file_path)
|
|
111
|
+
|
|
112
|
+
if not file_path.exists():
|
|
113
|
+
raise ValueError(f"File not found: {file_path}")
|
|
114
|
+
|
|
115
|
+
if file_path.suffix.lower() not in ['.xls', '.xlsx']:
|
|
116
|
+
raise ValueError(f"Not an Excel file: {file_path}")
|
|
117
|
+
|
|
118
|
+
# Try to import pandas here to avoid making it a required dependency
|
|
119
|
+
try:
|
|
120
|
+
import pandas as pd
|
|
121
|
+
except ImportError:
|
|
122
|
+
raise ValueError("pandas is required to read Excel files. Install it with: pip install pandas openpyxl")
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
df = pd.read_excel(file_path)
|
|
126
|
+
|
|
127
|
+
if df.empty:
|
|
128
|
+
raise ValueError("Excel file is empty")
|
|
129
|
+
|
|
130
|
+
# Convert all rows to dictionaries
|
|
131
|
+
result = []
|
|
132
|
+
for _, row in df.iterrows():
|
|
133
|
+
row_dict = {}
|
|
134
|
+
for key, value in row.items():
|
|
135
|
+
if pd.isna(value):
|
|
136
|
+
row_dict[key] = ""
|
|
137
|
+
else:
|
|
138
|
+
row_dict[key] = value
|
|
139
|
+
result.append(row_dict)
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
except Exception as e:
|
|
143
|
+
raise ValueError(f"Excel parsing error: {e}")
|
|
144
|
+
|
|
145
|
+
def parse_data_string(data_string: str) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
|
|
146
|
+
"""
|
|
147
|
+
Parse a data string in the format "{ 'key1': value1, 'key2': value2, ... }"
|
|
148
|
+
or a list of such objects "[{ 'key1': value1, ... }, { 'key2': value2, ... }]"
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
data_string (str): The data string to parse
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Union[List[Dict[str, Any]], Dict[str, Any]]: The parsed data
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ValueError: If the string cannot be parsed
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
# Clean up the input string to make it valid JSON
|
|
161
|
+
# Replace single quotes with double quotes
|
|
162
|
+
data_string = data_string.replace("'", '"')
|
|
163
|
+
|
|
164
|
+
# Handle Yes/No values (convert to true/false for JSON)
|
|
165
|
+
data_string = data_string.replace(': Yes', ': true').replace(': No', ': false')
|
|
166
|
+
|
|
167
|
+
# Add quotes around unquoted keys
|
|
168
|
+
import re
|
|
169
|
+
data_string = re.sub(r'(\{|\,)\s*([a-zA-Z0-9_]+)\s*:', r'\1 "\2":', data_string)
|
|
170
|
+
|
|
171
|
+
# Parse the resulting JSON
|
|
172
|
+
return json.loads(data_string)
|
|
173
|
+
except json.JSONDecodeError as e:
|
|
174
|
+
raise ValueError(f"Invalid data string format: {e}")
|
|
175
|
+
|
|
176
|
+
def load_file_based_on_extension(file_path: str) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
|
|
177
|
+
"""
|
|
178
|
+
Load data from a file based on its extension.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
file_path (str): Path to the file
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Union[List[Dict[str, Any]], Dict[str, Any]]: The loaded data - either a single entry or a list of entries
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
ValueError: If the file type is not supported
|
|
188
|
+
"""
|
|
189
|
+
file_path = Path(file_path)
|
|
190
|
+
|
|
191
|
+
if not file_path.exists():
|
|
192
|
+
raise ValueError(f"File not found: {file_path}")
|
|
193
|
+
|
|
194
|
+
suffix = file_path.suffix.lower()
|
|
195
|
+
|
|
196
|
+
if suffix == '.json':
|
|
197
|
+
return load_json_file(file_path)
|
|
198
|
+
elif suffix == '.csv':
|
|
199
|
+
return load_csv_file(file_path)
|
|
200
|
+
elif suffix in ['.xls', '.xlsx']:
|
|
201
|
+
return load_excel_file(file_path)
|
|
202
|
+
else:
|
|
203
|
+
raise ValueError(f"Unsupported file type: {suffix}. Supported types are: .json, .csv, .xls, .xlsx")
|
das/managers/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from das.common.config import load_api_url
|
|
4
|
+
from das.services.downloads import DownloadRequestService
|
|
5
|
+
from das.services.entries import EntriesService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DownloadManager:
|
|
9
|
+
|
|
10
|
+
def __init__(self):
|
|
11
|
+
base_url = load_api_url()
|
|
12
|
+
|
|
13
|
+
if (base_url is None or base_url == ""):
|
|
14
|
+
raise ValueError(f"Base URL is required - {self.__class__.__name__}")
|
|
15
|
+
|
|
16
|
+
self.download_request_service = DownloadRequestService(base_url)
|
|
17
|
+
self.entry_service = EntriesService(base_url)
|
|
18
|
+
|
|
19
|
+
def create_download_request(self, request_data: dict):
|
|
20
|
+
"""
|
|
21
|
+
Create a new download request.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
request_data (dict): A dictionary where keys are entry codes and values are lists of codes of the files. Also includes a 'name' key for the download request name.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
requests = {
|
|
28
|
+
'items': []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if 'name' not in request_data or not request_data['name']:
|
|
32
|
+
request_data['name'] = f"Download Request at {time.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
33
|
+
|
|
34
|
+
erros = []
|
|
35
|
+
|
|
36
|
+
for entry_code in request_data.keys():
|
|
37
|
+
# Validate that the entry exists
|
|
38
|
+
if entry_code == 'name':
|
|
39
|
+
continue
|
|
40
|
+
response = self.entry_service.get_entry(code=entry_code)
|
|
41
|
+
if response.get('entry', None) is None:
|
|
42
|
+
erros.append(f"Entry with code '{entry_code}' does not exist.")
|
|
43
|
+
else:
|
|
44
|
+
attribute_id = response.get('attributeId', None)
|
|
45
|
+
digital_objects_json = response.get('entry').get('6', None) # Assuming '6' is the attribute ID for digital objects
|
|
46
|
+
if digital_objects_json is None:
|
|
47
|
+
erros.append(f"Entry with code '{entry_code}' has no digital objects.")
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
# convert digital_objects_json to a array of dicts
|
|
51
|
+
digital_objects = []
|
|
52
|
+
try:
|
|
53
|
+
digital_objects = json.loads(digital_objects_json)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
erros.append(f"Error parsing digital objects for entry '{entry_code}': {str(e)}")
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
entry = response.get('entry')
|
|
59
|
+
|
|
60
|
+
if request_data[entry_code] is None or len(request_data[entry_code]) == 0:
|
|
61
|
+
filtered_digital_objects = digital_objects
|
|
62
|
+
else:
|
|
63
|
+
filtered_digital_objects = [obj for obj in digital_objects if obj.get('code') in request_data[entry_code]]
|
|
64
|
+
|
|
65
|
+
if len(filtered_digital_objects) == 0:
|
|
66
|
+
erros.append(f"No matching digital objects found for entry '{entry_code}' with the provided file codes.")
|
|
67
|
+
continue
|
|
68
|
+
for digital_object in filtered_digital_objects:
|
|
69
|
+
request = {
|
|
70
|
+
'name': request_data['name'],
|
|
71
|
+
'sourceId': entry.get('id'),
|
|
72
|
+
'sourceAttributeId': attribute_id,
|
|
73
|
+
'id': digital_object.get('id')
|
|
74
|
+
}
|
|
75
|
+
requests['items'].append(request)
|
|
76
|
+
|
|
77
|
+
if erros:
|
|
78
|
+
return {"errors": erros}
|
|
79
|
+
|
|
80
|
+
return self.download_request_service.create(requests)
|
|
81
|
+
|
|
82
|
+
def delete_download_request(self, request_id: str):
|
|
83
|
+
"""
|
|
84
|
+
Delete a download request by ID.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
request_id (str): The ID of the download request to delete.
|
|
88
|
+
"""
|
|
89
|
+
return self.download_request_service.delete(request_id)
|
|
90
|
+
|
|
91
|
+
def get_my_requests(self):
|
|
92
|
+
"""Get all download requests for the current user."""
|
|
93
|
+
return self.download_request_service.get_my_requests()
|