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/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()
@@ -0,0 +1,3 @@
1
+ # Constants for Entry Fields - Input Types
2
+ DIGITAL_OBJECT_INPUT = 13
3
+ SELECT_COMBO_INPUT = 4
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
@@ -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")
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()