das-cli 1.2.10__py3-none-any.whl → 1.2.12__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/cli.py +1245 -1245
- das/common/entry_fields_constants.py +2 -1
- das/managers/entries_manager.py +39 -11
- das/services/downloads.py +100 -100
- {das_cli-1.2.10.dist-info → das_cli-1.2.12.dist-info}/METADATA +1 -1
- {das_cli-1.2.10.dist-info → das_cli-1.2.12.dist-info}/RECORD +10 -10
- {das_cli-1.2.10.dist-info → das_cli-1.2.12.dist-info}/WHEEL +1 -1
- {das_cli-1.2.10.dist-info → das_cli-1.2.12.dist-info}/entry_points.txt +0 -0
- {das_cli-1.2.10.dist-info → das_cli-1.2.12.dist-info}/licenses/LICENSE +0 -0
- {das_cli-1.2.10.dist-info → das_cli-1.2.12.dist-info}/top_level.txt +0 -0
das/cli.py
CHANGED
|
@@ -1,1245 +1,1245 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import click
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from das.common.config import (
|
|
7
|
-
save_api_url, load_api_url, DEFAULT_BASE_URL,
|
|
8
|
-
save_verify_ssl, load_verify_ssl, VERIFY_SSL,
|
|
9
|
-
load_openai_api_key, save_openai_api_key, clear_openai_api_key,
|
|
10
|
-
clear_token, _config_dir
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
from das.app import Das
|
|
14
|
-
from das.managers.download_manager import DownloadManager
|
|
15
|
-
from das.managers.entries_manager import EntryManager
|
|
16
|
-
from das.managers.search_manager import SearchManager
|
|
17
|
-
from das.managers.digital_objects_manager import DigitalObjectsManager
|
|
18
|
-
from das.common.file_utils import load_file_based_on_extension, parse_data_string
|
|
19
|
-
from das.ai.plugins.dasai import DasAI
|
|
20
|
-
|
|
21
|
-
# Simple table formatting function to avoid external dependencies
|
|
22
|
-
def format_table(data, headers):
|
|
23
|
-
"""Format data into a text-based table without external dependencies"""
|
|
24
|
-
if not data:
|
|
25
|
-
return "No data to display"
|
|
26
|
-
|
|
27
|
-
# Calculate column widths
|
|
28
|
-
col_widths = [max(len(str(h)), max([len(str(row[i])) for row in data] or [0])) for i, h in enumerate(headers)]
|
|
29
|
-
|
|
30
|
-
# Create the separator line
|
|
31
|
-
separator = '+' + '+'.join('-' * (w + 2) for w in col_widths) + '+'
|
|
32
|
-
|
|
33
|
-
# Create the header row
|
|
34
|
-
header_row = '|' + '|'.join(' ' + str(h).ljust(w) + ' ' for h, w in zip(headers, col_widths)) + '|'
|
|
35
|
-
|
|
36
|
-
# Create the data rows
|
|
37
|
-
data_rows = []
|
|
38
|
-
for row in data:
|
|
39
|
-
data_rows.append('|' + '|'.join(' ' + str(cell).ljust(w) + ' ' for cell, w in zip(row, col_widths)) + '|')
|
|
40
|
-
|
|
41
|
-
# Assemble the table
|
|
42
|
-
table = [separator, header_row, separator]
|
|
43
|
-
table.extend(data_rows)
|
|
44
|
-
table.append(separator)
|
|
45
|
-
|
|
46
|
-
return '\n'.join(table)
|
|
47
|
-
|
|
48
|
-
class DasCLI:
|
|
49
|
-
def __init__(self):
|
|
50
|
-
self.client = None
|
|
51
|
-
self.api_url = None
|
|
52
|
-
self.entry_manager = None
|
|
53
|
-
self.search_manager = None
|
|
54
|
-
self.download_manager = None
|
|
55
|
-
self.digital_objects_manager = None
|
|
56
|
-
self.das_ai = None
|
|
57
|
-
|
|
58
|
-
def get_client(self):
|
|
59
|
-
"""Get DAS client instance, using saved API URL"""
|
|
60
|
-
if not self.api_url:
|
|
61
|
-
self.api_url = load_api_url() or DEFAULT_BASE_URL
|
|
62
|
-
if not self.api_url:
|
|
63
|
-
raise click.UsageError(
|
|
64
|
-
"No API URL configured. Please login first with 'das login --api-url <URL>'"
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
self.entry_manager = EntryManager()
|
|
68
|
-
self.search_manager = SearchManager()
|
|
69
|
-
self.download_manager = DownloadManager()
|
|
70
|
-
self.digital_objects_manager = DigitalObjectsManager()
|
|
71
|
-
# Set SSL verification based on saved config
|
|
72
|
-
if not self.client:
|
|
73
|
-
self.client = Das(self.api_url)
|
|
74
|
-
|
|
75
|
-
return self.client
|
|
76
|
-
|
|
77
|
-
def get_das_ai(self):
|
|
78
|
-
"""Get DAS AI instance"""
|
|
79
|
-
if not self.das_ai:
|
|
80
|
-
self.das_ai = DasAI()
|
|
81
|
-
return self.das_ai
|
|
82
|
-
|
|
83
|
-
pass_das_context = click.make_pass_decorator(DasCLI, ensure=True)
|
|
84
|
-
|
|
85
|
-
@click.group()
|
|
86
|
-
@click.version_option(package_name="das-cli")
|
|
87
|
-
@click.pass_context
|
|
88
|
-
def cli(ctx):
|
|
89
|
-
"""DAS Python CLI - Data Archive System client tool"""
|
|
90
|
-
ctx.obj = DasCLI()
|
|
91
|
-
|
|
92
|
-
@cli.command()
|
|
93
|
-
@click.option('--api-url', required=True, help='API base URL')
|
|
94
|
-
@click.option('--username', required=True, prompt=True, help='Username')
|
|
95
|
-
@click.option('--password', required=True, prompt=True, hide_input=True, help='Password')
|
|
96
|
-
@pass_das_context
|
|
97
|
-
def login(das_ctx, api_url, username, password):
|
|
98
|
-
"""Login and store authentication token"""
|
|
99
|
-
# Save API URL for future use
|
|
100
|
-
save_api_url(api_url)
|
|
101
|
-
das_ctx.api_url = api_url
|
|
102
|
-
|
|
103
|
-
# Authenticate
|
|
104
|
-
client = das_ctx.get_client()
|
|
105
|
-
token = client.authenticate(username, password)
|
|
106
|
-
if not token:
|
|
107
|
-
click.secho("❌ Authentication failed. Please check your credentials.", fg="red")
|
|
108
|
-
return
|
|
109
|
-
else:
|
|
110
|
-
click.secho("✓ Authentication successful!", fg="green")
|
|
111
|
-
|
|
112
|
-
# Search commands group
|
|
113
|
-
@cli.group()
|
|
114
|
-
def search():
|
|
115
|
-
"""Commands for searching entries"""
|
|
116
|
-
pass
|
|
117
|
-
|
|
118
|
-
@search.command("help")
|
|
119
|
-
def search_help():
|
|
120
|
-
"""Show help about search query syntax and formats"""
|
|
121
|
-
click.secho("\nSearch Query Syntax Help", fg="green", bold=True)
|
|
122
|
-
click.echo("=" * 50)
|
|
123
|
-
|
|
124
|
-
click.secho("\nBasic Search:", fg="blue", bold=True)
|
|
125
|
-
click.echo(" das search entries --attribute <AttributeName> --query '<SearchQuery>'")
|
|
126
|
-
click.echo(" Example: das search entries --attribute Cores --query 'name(*64*)'")
|
|
127
|
-
|
|
128
|
-
click.secho("\nOutput Formats:", fg="blue", bold=True)
|
|
129
|
-
click.echo(" --format table Format results in a nice table (default)")
|
|
130
|
-
click.echo(" --format json Return results as JSON")
|
|
131
|
-
click.echo(" --format compact Simple one-line-per-result format")
|
|
132
|
-
click.echo(" --raw Show raw API response")
|
|
133
|
-
|
|
134
|
-
click.secho("\nQuery Syntax Examples:", fg="blue", bold=True)
|
|
135
|
-
click.echo(" 'name(*pattern*)' Search for pattern in name")
|
|
136
|
-
click.echo(" 'name(*pattern*);code(*ABC*)' Multiple conditions (AND)")
|
|
137
|
-
click.echo(" 'Create at(>2023-01-01)' Date comparison")
|
|
138
|
-
|
|
139
|
-
click.secho("\nGet Detailed Information:", fg="blue", bold=True)
|
|
140
|
-
click.echo(" das search entry <ID> Get detailed info for a single entry")
|
|
141
|
-
click.echo(" Example: das search entry 6b0e68e6-00cd-43a7-9c51-d56c9c091123")
|
|
142
|
-
|
|
143
|
-
click.secho("\nPagination:", fg="blue", bold=True)
|
|
144
|
-
click.echo(" --page <num> Show specific page of results")
|
|
145
|
-
click.echo(" --max-results <n> Number of results per page (default: 10)")
|
|
146
|
-
|
|
147
|
-
click.secho("\nSorting:", fg="blue", bold=True)
|
|
148
|
-
click.echo(" --sort-by <field> Field to sort by (default: Name)")
|
|
149
|
-
click.echo(" --sort-order <order> Sort order: 'asc' or 'desc' (default: asc)")
|
|
150
|
-
click.echo("\n")
|
|
151
|
-
|
|
152
|
-
@search.command("entries")
|
|
153
|
-
@click.option('--attribute', required=True, help='Attribute name to search in')
|
|
154
|
-
@click.option('--query', required=True, help='Search query string')
|
|
155
|
-
@click.option('--max-results', default=10, help='Maximum number of results to return')
|
|
156
|
-
@click.option('--page', default=1, help='Page number for paginated results')
|
|
157
|
-
@click.option('--sort-by', default='Name', help='Field to sort by (default: Name)')
|
|
158
|
-
@click.option('--sort-order', default='asc', type=click.Choice(['asc', 'desc']), help='Sort order (asc or desc)')
|
|
159
|
-
@click.option('--format', 'output_format', default='table', type=click.Choice(['table', 'json', 'compact']), help='Output format (table, json, or compact)')
|
|
160
|
-
@click.option('--raw', is_flag=True, help='Show raw response from API')
|
|
161
|
-
@pass_das_context
|
|
162
|
-
def search_entries(das_ctx, attribute, query, max_results, page, sort_by, sort_order, output_format, raw):
|
|
163
|
-
"""Search entries based on attribute and query"""
|
|
164
|
-
try:
|
|
165
|
-
# Ensure client and search_manager are initialized
|
|
166
|
-
das_ctx.get_client()
|
|
167
|
-
|
|
168
|
-
results = das_ctx.search_manager.search_entries(
|
|
169
|
-
attribute=attribute,
|
|
170
|
-
query=query,
|
|
171
|
-
max_results=max_results,
|
|
172
|
-
page=page,
|
|
173
|
-
sort_by=sort_by,
|
|
174
|
-
sort_order=sort_order
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
if raw:
|
|
178
|
-
click.echo(results)
|
|
179
|
-
return
|
|
180
|
-
|
|
181
|
-
total_count = results.get('totalCount', 0)
|
|
182
|
-
items = results.get('items', [])
|
|
183
|
-
|
|
184
|
-
# Display search summary
|
|
185
|
-
click.secho(f"\nFound {total_count} results for query: '{query}' in attribute: '{attribute}'", fg="blue")
|
|
186
|
-
click.secho(f"Showing results {(page-1)*max_results + 1}-{min(page*max_results, total_count)} of {total_count}\n", fg="blue")
|
|
187
|
-
|
|
188
|
-
if not items:
|
|
189
|
-
click.secho("No results found.", fg="yellow")
|
|
190
|
-
return
|
|
191
|
-
|
|
192
|
-
if output_format == 'json':
|
|
193
|
-
click.echo(json.dumps(results, indent=2))
|
|
194
|
-
elif output_format == 'compact':
|
|
195
|
-
for i, item in enumerate(items, 1):
|
|
196
|
-
entry = item.get('entry', {})
|
|
197
|
-
code = entry.get('code', 'N/A')
|
|
198
|
-
name = entry.get('displayname', 'Unnamed')
|
|
199
|
-
description = entry.get('description', 'No description')
|
|
200
|
-
click.echo(f"{i}. {name} (Code: {code}) - {description}")
|
|
201
|
-
else: # table format
|
|
202
|
-
headers = ["#", "Code", "Name", "Description", "Owner"]
|
|
203
|
-
table_data = []
|
|
204
|
-
|
|
205
|
-
for i, item in enumerate(items, 1):
|
|
206
|
-
entry = item.get('entry', {})
|
|
207
|
-
table_data.append([
|
|
208
|
-
i,
|
|
209
|
-
entry.get('code', 'N/A'),
|
|
210
|
-
entry.get('displayname', 'Unnamed'),
|
|
211
|
-
entry.get('description', 'No description')[:50] + ('...' if entry.get('description', '') and len(entry.get('description', '')) > 50 else ''),
|
|
212
|
-
entry.get('owner', 'Unknown')
|
|
213
|
-
])
|
|
214
|
-
|
|
215
|
-
click.echo(format_table(table_data, headers))
|
|
216
|
-
|
|
217
|
-
except Exception as e:
|
|
218
|
-
click.secho(f"Error: {e}", fg="red")
|
|
219
|
-
|
|
220
|
-
# Hangfire commands group
|
|
221
|
-
@search.command("entry")
|
|
222
|
-
@click.argument('entry_id', required=True)
|
|
223
|
-
@click.option('--format', 'output_format', default='table', type=click.Choice(['table', 'json']), help='Output format (table or json)')
|
|
224
|
-
@pass_das_context
|
|
225
|
-
def get_entry(das_ctx, entry_id, output_format):
|
|
226
|
-
"""Get detailed information about a single entry by ID"""
|
|
227
|
-
try:
|
|
228
|
-
# Ensure client and entry_manager are initialized
|
|
229
|
-
das_ctx.get_client()
|
|
230
|
-
# Reusing the entry manager to get entry details by ID
|
|
231
|
-
entry = das_ctx.entry_manager.get_entry(entry_id)
|
|
232
|
-
|
|
233
|
-
if output_format == 'json':
|
|
234
|
-
click.echo(json.dumps(entry, indent=2))
|
|
235
|
-
else:
|
|
236
|
-
click.secho(f"\nEntry Details (ID: {entry_id})", fg="blue", bold=True)
|
|
237
|
-
click.echo("=" * 50)
|
|
238
|
-
|
|
239
|
-
if not entry or not isinstance(entry, dict):
|
|
240
|
-
click.secho("No entry found with the specified ID.", fg="yellow")
|
|
241
|
-
return
|
|
242
|
-
|
|
243
|
-
# Format display fields in a readable way
|
|
244
|
-
formatted_data = []
|
|
245
|
-
for key, value in sorted(entry.items()):
|
|
246
|
-
# Skip empty or None values
|
|
247
|
-
if value is None or value == '':
|
|
248
|
-
continue
|
|
249
|
-
|
|
250
|
-
# Format date fields
|
|
251
|
-
if isinstance(value, str) and key.lower().endswith(('time', 'date')) and 'T' in value:
|
|
252
|
-
try:
|
|
253
|
-
date_part = value.split('T')[0]
|
|
254
|
-
time_part = value.split('T')[1].split('.')[0]
|
|
255
|
-
value = f"{date_part} {time_part}"
|
|
256
|
-
except:
|
|
257
|
-
pass # Keep original if formatting fails
|
|
258
|
-
|
|
259
|
-
# Add to formatted data
|
|
260
|
-
formatted_data.append((key, value))
|
|
261
|
-
|
|
262
|
-
# Display as key-value pairs
|
|
263
|
-
for key, value in formatted_data:
|
|
264
|
-
click.echo(f"{key.ljust(25)}: {value}")
|
|
265
|
-
|
|
266
|
-
except Exception as e:
|
|
267
|
-
click.secho(f"Error: {e}", fg="red")
|
|
268
|
-
|
|
269
|
-
@cli.group()
|
|
270
|
-
def hangfire():
|
|
271
|
-
"""Commands for working with Hangfire tasks"""
|
|
272
|
-
pass
|
|
273
|
-
|
|
274
|
-
@hangfire.command("sync-doi")
|
|
275
|
-
@click.argument('id', required=True)
|
|
276
|
-
@pass_das_context
|
|
277
|
-
def sync_doi(das_ctx, id):
|
|
278
|
-
"""Trigger a DOI synchronization task by ID"""
|
|
279
|
-
client = das_ctx.get_client()
|
|
280
|
-
try:
|
|
281
|
-
client.hangfire.sync_doi(id)
|
|
282
|
-
click.secho(f"✓ DOI synchronization task triggered!", fg="green")
|
|
283
|
-
except Exception as e:
|
|
284
|
-
click.secho(f"Error: {e}", fg="red")
|
|
285
|
-
|
|
286
|
-
# Entries commands group
|
|
287
|
-
@cli.group()
|
|
288
|
-
def entry():
|
|
289
|
-
"""Commands for working with entries"""
|
|
290
|
-
pass
|
|
291
|
-
|
|
292
|
-
@entry.command("update")
|
|
293
|
-
@click.option('--id', default=None, help='Entry ID to update (only used with --data for single entry)')
|
|
294
|
-
@click.option('--code', default=None, help='Entry code to update (only used with --data for single entry)')
|
|
295
|
-
@click.argument('file_path', required=False)
|
|
296
|
-
@click.option('--data', help='Data string in format { "key1": "value1", "key2": "value2", ... } or a list of such objects')
|
|
297
|
-
@pass_das_context
|
|
298
|
-
def update_entry(das_ctx, id, code, file_path, data):
|
|
299
|
-
"""Update entries from file or data string by ID or code.
|
|
300
|
-
|
|
301
|
-
For single updates, provide either --id or --code (or Id/Code in the data).
|
|
302
|
-
For bulk updates, each entry must contain a Code or Id field.
|
|
303
|
-
Files can contain multiple entries (rows in CSV/XLS or list of objects in JSON).
|
|
304
|
-
|
|
305
|
-
Examples:
|
|
306
|
-
|
|
307
|
-
\b
|
|
308
|
-
# Update by code from data string
|
|
309
|
-
das entry update --code ENT001 --data "{ 'Grant Public Access': 'Yes' }"
|
|
310
|
-
|
|
311
|
-
\b
|
|
312
|
-
# Update by ID from data string
|
|
313
|
-
das entry update --id 9b826efc-36f3-471c-ad18-9c1e00abe4fa --data "{ 'Name': 'Updated' }"
|
|
314
|
-
|
|
315
|
-
\b
|
|
316
|
-
# Update entries from JSON file (each object needs Code or Id)
|
|
317
|
-
das entry update c:\\data\\entries.json
|
|
318
|
-
|
|
319
|
-
\b
|
|
320
|
-
# Update entries from CSV file
|
|
321
|
-
das entry update c:\\data\\entries.csv
|
|
322
|
-
|
|
323
|
-
\b
|
|
324
|
-
# Update multiple entries from data string
|
|
325
|
-
das entry update --data "[{ 'Code': 'ENT001', ... }, { 'Code': 'ENT002', ... }]"
|
|
326
|
-
"""
|
|
327
|
-
try:
|
|
328
|
-
# Ensure client and entry_manager are initialized
|
|
329
|
-
das_ctx.get_client()
|
|
330
|
-
if not file_path and not data:
|
|
331
|
-
raise click.UsageError("Please provide either a file path or data string")
|
|
332
|
-
|
|
333
|
-
entry_data = None
|
|
334
|
-
is_bulk_update = False
|
|
335
|
-
|
|
336
|
-
if file_path:
|
|
337
|
-
click.echo(f"Loading data from file: {file_path}")
|
|
338
|
-
entry_data = load_file_based_on_extension(file_path)
|
|
339
|
-
|
|
340
|
-
# Check if we got a list or a single object
|
|
341
|
-
if isinstance(entry_data, list):
|
|
342
|
-
is_bulk_update = True
|
|
343
|
-
click.echo(f"Found {len(entry_data)} entries to update")
|
|
344
|
-
else:
|
|
345
|
-
# If we got a single object but no id/code was provided, check if it has Id or Code in data
|
|
346
|
-
if not id and not code:
|
|
347
|
-
data_id = next((entry_data.get(key) for key in entry_data if key.lower() == 'id'), None)
|
|
348
|
-
data_code = next((entry_data.get(key) for key in entry_data if key.lower() == 'code'), None)
|
|
349
|
-
if not data_id and not data_code:
|
|
350
|
-
raise click.UsageError("No --id or --code provided and entry data doesn't contain an Id or Code field")
|
|
351
|
-
id = id or data_id
|
|
352
|
-
code = code or data_code
|
|
353
|
-
|
|
354
|
-
elif data:
|
|
355
|
-
click.echo("Parsing data string")
|
|
356
|
-
entry_data = parse_data_string(data)
|
|
357
|
-
|
|
358
|
-
# Check if we got a list or a single object
|
|
359
|
-
if isinstance(entry_data, list):
|
|
360
|
-
is_bulk_update = True
|
|
361
|
-
click.echo(f"Found {len(entry_data)} entries to update")
|
|
362
|
-
elif not id and not code:
|
|
363
|
-
# If we got a single object but no id/code was provided, check if it has Id or Code in data
|
|
364
|
-
data_id = next((entry_data.get(key) for key in entry_data if key.lower() == 'id'), None)
|
|
365
|
-
data_code = next((entry_data.get(key) for key in entry_data if key.lower() == 'code'), None)
|
|
366
|
-
if not data_id and not data_code:
|
|
367
|
-
raise click.UsageError("No --id or --code provided and data doesn't contain an Id or Code field")
|
|
368
|
-
id = id or data_id
|
|
369
|
-
code = code or data_code
|
|
370
|
-
|
|
371
|
-
if not entry_data:
|
|
372
|
-
raise click.UsageError("No valid entry data found")
|
|
373
|
-
|
|
374
|
-
# For single entry update, require id or code
|
|
375
|
-
if not is_bulk_update and not id and not code:
|
|
376
|
-
raise click.UsageError("Please provide either --id or --code, or include Id or Code in the data")
|
|
377
|
-
|
|
378
|
-
# Update the entries
|
|
379
|
-
if is_bulk_update:
|
|
380
|
-
results = das_ctx.entry_manager.update(entries=entry_data)
|
|
381
|
-
|
|
382
|
-
# Display results
|
|
383
|
-
success_count = sum(1 for result in results if result.get('status') == 'success')
|
|
384
|
-
error_count = len(results) - success_count
|
|
385
|
-
|
|
386
|
-
if success_count > 0:
|
|
387
|
-
click.secho(f"✓ Successfully updated {success_count} entries", fg="green")
|
|
388
|
-
|
|
389
|
-
if error_count > 0:
|
|
390
|
-
click.secho(f"✗ Failed to update {error_count} entries", fg="red")
|
|
391
|
-
|
|
392
|
-
# Show details of the failures
|
|
393
|
-
click.echo("\nFailed updates:")
|
|
394
|
-
for result in results:
|
|
395
|
-
if result.get('status') == 'error':
|
|
396
|
-
ident = result.get('code') or result.get('id') or 'Unknown'
|
|
397
|
-
click.echo(f" {ident}: {result.get('error', 'Unknown error')}")
|
|
398
|
-
else:
|
|
399
|
-
# Single entry update
|
|
400
|
-
results = das_ctx.entry_manager.update(id=id, code=code, entry=entry_data)
|
|
401
|
-
|
|
402
|
-
if results and results[0].get('status') == 'success':
|
|
403
|
-
identifier = id or code
|
|
404
|
-
id_label = "ID" if id else "code"
|
|
405
|
-
click.secho(f"✓ Entry '{identifier}' ({id_label}) updated successfully!", fg="green")
|
|
406
|
-
click.echo(f"Entry ID: {results[0].get('id')}")
|
|
407
|
-
else:
|
|
408
|
-
error_msg = results[0].get('error', 'No response from server') if results else 'No response from server'
|
|
409
|
-
click.secho(f"Entry update failed: {error_msg}", fg="red")
|
|
410
|
-
|
|
411
|
-
except Exception as e:
|
|
412
|
-
click.secho(f"Error: {e}", fg="red")
|
|
413
|
-
|
|
414
|
-
@entry.command("delete")
|
|
415
|
-
@click.option('--code', default=None, help='Entry code')
|
|
416
|
-
@click.option('--id', default=None, help='Entry ID')
|
|
417
|
-
@click.option('--force', is_flag=True, help='Skip confirmation prompt')
|
|
418
|
-
@pass_das_context
|
|
419
|
-
def delete_entry(das_ctx, code, id, force):
|
|
420
|
-
"""Delete an entry by its code or ID"""
|
|
421
|
-
if not code and not id:
|
|
422
|
-
raise click.UsageError("Please provide either an entry code or ID")
|
|
423
|
-
|
|
424
|
-
# Ensure client and entry_manager are initialized
|
|
425
|
-
das_ctx.get_client()
|
|
426
|
-
|
|
427
|
-
identifier = code or id
|
|
428
|
-
id_label = "code" if code else "ID"
|
|
429
|
-
|
|
430
|
-
if not force:
|
|
431
|
-
if not click.confirm(f"Are you sure you want to delete entry with {id_label} '{identifier}'?"):
|
|
432
|
-
click.echo("Operation cancelled.")
|
|
433
|
-
return
|
|
434
|
-
|
|
435
|
-
try:
|
|
436
|
-
das_ctx.entry_manager.delete(id=id, code=code)
|
|
437
|
-
click.secho(f"✓ Entry with {id_label} '{identifier}' deleted!", fg="green")
|
|
438
|
-
except Exception as e:
|
|
439
|
-
click.secho(f"Error: {e}", fg="red")
|
|
440
|
-
|
|
441
|
-
@entry.command("link-digital-objects")
|
|
442
|
-
@click.option('--entry-code', required=True, help='Entry code to link/unlink digital objects to')
|
|
443
|
-
@click.option('--digital-object-code', '-d', multiple=True, required=True, help='Digital object code. Use multiple times for multiple objects.')
|
|
444
|
-
@click.option('--unlink', is_flag=True, help='Unlink specified digital objects from the entry instead of linking')
|
|
445
|
-
@pass_das_context
|
|
446
|
-
def link_digital_objects(das_ctx, entry_code, digital_object_code, unlink):
|
|
447
|
-
"""Link or unlink existing digital objects to an entry by their codes.
|
|
448
|
-
|
|
449
|
-
Examples:
|
|
450
|
-
|
|
451
|
-
\b
|
|
452
|
-
# Link two digital objects to an entry
|
|
453
|
-
das entry link-digital-objects --entry-code ENT001 -d DO001 -d DO002
|
|
454
|
-
|
|
455
|
-
\b
|
|
456
|
-
# Unlink a digital object from an entry
|
|
457
|
-
das entry link-digital-objects --entry-code ENT001 -d DO003 --unlink
|
|
458
|
-
"""
|
|
459
|
-
try:
|
|
460
|
-
das_ctx.get_client()
|
|
461
|
-
codes = list(digital_object_code)
|
|
462
|
-
if not codes:
|
|
463
|
-
raise click.UsageError("Please provide at least one --digital-object-code")
|
|
464
|
-
|
|
465
|
-
success = das_ctx.digital_objects_manager.link_existing_digital_objects(
|
|
466
|
-
entry_code=entry_code,
|
|
467
|
-
digital_object_code_list=codes,
|
|
468
|
-
is_unlink=unlink,
|
|
469
|
-
)
|
|
470
|
-
|
|
471
|
-
if success:
|
|
472
|
-
action = "unlinked" if unlink else "linked"
|
|
473
|
-
click.secho(f"✓ Successfully {action} {len(codes)} digital object(s) for entry '{entry_code}'", fg="green")
|
|
474
|
-
else:
|
|
475
|
-
click.secho("Operation did not report success.", fg="yellow")
|
|
476
|
-
except Exception as e:
|
|
477
|
-
click.secho(f"Error: {e}", fg="red")
|
|
478
|
-
|
|
479
|
-
@entry.command("create")
|
|
480
|
-
@click.option('--attribute', required=True, help='Attribute name')
|
|
481
|
-
@click.argument('file_path', required=False)
|
|
482
|
-
@click.option('--data', help='Data string in format { "key1": "value1", "key2": "value2", ... } or a list of such objects')
|
|
483
|
-
@pass_das_context
|
|
484
|
-
def create_entry(das_ctx, attribute, file_path=None, data=None):
|
|
485
|
-
"""Create one or more entries from file or data string
|
|
486
|
-
|
|
487
|
-
Files can contain multiple entries (rows in CSV/XLS or list of objects in JSON).
|
|
488
|
-
|
|
489
|
-
Examples:
|
|
490
|
-
|
|
491
|
-
\b
|
|
492
|
-
# Create entries from JSON file
|
|
493
|
-
das entry create --attribute core c:\\data\\entries.json
|
|
494
|
-
|
|
495
|
-
\b
|
|
496
|
-
# Create entries from CSV file
|
|
497
|
-
das entry create --attribute core c:\\data\\entries.csv
|
|
498
|
-
|
|
499
|
-
\b
|
|
500
|
-
# Create entries from Excel file
|
|
501
|
-
das entry create --attribute core c:\\data\\entries.xls
|
|
502
|
-
|
|
503
|
-
\b
|
|
504
|
-
# Create a single entry from data string
|
|
505
|
-
das entry create --attribute core --data { 'Grant Public Access': Yes, ... }
|
|
506
|
-
|
|
507
|
-
\b
|
|
508
|
-
# Create multiple entries from data string
|
|
509
|
-
das entry create --attribute core --data [{ 'Name': 'Entry 1', ... }, { 'Name': 'Entry 2', ... }]
|
|
510
|
-
"""
|
|
511
|
-
try:
|
|
512
|
-
# Ensure client and entry_manager are initialized
|
|
513
|
-
das_ctx.get_client()
|
|
514
|
-
if not file_path and not data:
|
|
515
|
-
raise click.UsageError("Please provide either a file path or data string")
|
|
516
|
-
|
|
517
|
-
entry_data = None
|
|
518
|
-
is_bulk_create = False
|
|
519
|
-
|
|
520
|
-
if file_path:
|
|
521
|
-
click.echo(f"Loading data from file: {file_path}")
|
|
522
|
-
entry_data = load_file_based_on_extension(file_path)
|
|
523
|
-
|
|
524
|
-
# Check if we got a list or a single object
|
|
525
|
-
if isinstance(entry_data, list):
|
|
526
|
-
is_bulk_create = True
|
|
527
|
-
click.echo(f"Found {len(entry_data)} entries to create")
|
|
528
|
-
|
|
529
|
-
elif data:
|
|
530
|
-
click.echo("Parsing data string")
|
|
531
|
-
entry_data = parse_data_string(data)
|
|
532
|
-
|
|
533
|
-
# Check if we got a list or a single object
|
|
534
|
-
if isinstance(entry_data, list):
|
|
535
|
-
is_bulk_create = True
|
|
536
|
-
click.echo(f"Found {len(entry_data)} entries to create")
|
|
537
|
-
|
|
538
|
-
if not entry_data:
|
|
539
|
-
raise click.UsageError("No valid entry data found")
|
|
540
|
-
|
|
541
|
-
# Create the entries
|
|
542
|
-
if is_bulk_create:
|
|
543
|
-
results = das_ctx.entry_manager.create(attribute=attribute, entries=entry_data)
|
|
544
|
-
|
|
545
|
-
# Display results
|
|
546
|
-
success_count = sum(1 for result in results if result.get('status') == 'success')
|
|
547
|
-
error_count = len(results) - success_count
|
|
548
|
-
|
|
549
|
-
if success_count > 0:
|
|
550
|
-
click.secho(f"✓ Successfully created {success_count} entries", fg="green")
|
|
551
|
-
|
|
552
|
-
# Show IDs of created entries
|
|
553
|
-
click.echo("\nCreated entry IDs:")
|
|
554
|
-
for result in results:
|
|
555
|
-
if result.get('status') == 'success':
|
|
556
|
-
click.echo(f" {result.get('id')}")
|
|
557
|
-
|
|
558
|
-
if error_count > 0:
|
|
559
|
-
click.secho(f"✗ Failed to create {error_count} entries", fg="red")
|
|
560
|
-
|
|
561
|
-
# Show details of the failures
|
|
562
|
-
click.echo("\nFailed creations:")
|
|
563
|
-
for i, result in enumerate(results):
|
|
564
|
-
if result.get('status') == 'error':
|
|
565
|
-
click.echo(f" Entry #{i+1}: {result.get('error', 'Unknown error')}")
|
|
566
|
-
else:
|
|
567
|
-
# Single entry creation
|
|
568
|
-
results = das_ctx.entry_manager.create(attribute=attribute, entry=entry_data)
|
|
569
|
-
|
|
570
|
-
if results and results[0].get('status') == 'success':
|
|
571
|
-
click.secho(f"✓ Entry created successfully!", fg="green")
|
|
572
|
-
click.echo(f"Entry ID: {results[0].get('id')}")
|
|
573
|
-
else:
|
|
574
|
-
error_msg = results[0].get('error', 'No ID returned') if results else 'No ID returned'
|
|
575
|
-
click.secho(f"Entry creation failed: {error_msg}", fg="red")
|
|
576
|
-
|
|
577
|
-
except Exception as e:
|
|
578
|
-
click.secho(f"Error: {e}", fg="red")
|
|
579
|
-
|
|
580
|
-
@entry.command("upload-digital-object")
|
|
581
|
-
@click.option('--entry-code', required=True, help='Entry code to attach the digital object to')
|
|
582
|
-
@click.option('--type', 'digital_object_type', required=True, help='Digital object type name (e.g., Dataset, File, Image)')
|
|
583
|
-
@click.option('--description', 'file_description', default='', help='Description for the uploaded file')
|
|
584
|
-
@click.argument('file_path', required=True)
|
|
585
|
-
@pass_das_context
|
|
586
|
-
def upload_digital_object(das_ctx, entry_code, digital_object_type, file_description, file_path):
|
|
587
|
-
"""Upload a file as a digital object and link it to an entry.
|
|
588
|
-
|
|
589
|
-
Examples:
|
|
590
|
-
|
|
591
|
-
\b
|
|
592
|
-
# Upload a dataset file and link to an entry
|
|
593
|
-
das entry upload-digital-object --entry-code ENT001 --type Dataset --description "CTD raw" c:\\data\\ctd.zip
|
|
594
|
-
"""
|
|
595
|
-
try:
|
|
596
|
-
# Ensure services are initialized
|
|
597
|
-
das_ctx.get_client()
|
|
598
|
-
|
|
599
|
-
# Perform upload and link
|
|
600
|
-
digital_object_id = das_ctx.digital_objects_manager.upload_digital_object(
|
|
601
|
-
entry_code=entry_code,
|
|
602
|
-
file_description=file_description,
|
|
603
|
-
digital_object_type=digital_object_type,
|
|
604
|
-
file_path=file_path,
|
|
605
|
-
)
|
|
606
|
-
|
|
607
|
-
if digital_object_id:
|
|
608
|
-
click.secho("✓ Digital object uploaded and linked successfully!", fg="green")
|
|
609
|
-
click.echo(f"Digital Object ID: {digital_object_id}")
|
|
610
|
-
else:
|
|
611
|
-
click.secho("Upload completed but no ID was returned.", fg="yellow")
|
|
612
|
-
except Exception as e:
|
|
613
|
-
click.secho(f"Error: {e}", fg="red")
|
|
614
|
-
|
|
615
|
-
@entry.command("get")
|
|
616
|
-
@click.option('--code', default=None, help='Entry code')
|
|
617
|
-
@click.option('--id', type=int, default=None, help='Entry ID')
|
|
618
|
-
@pass_das_context
|
|
619
|
-
def get_entry(das_ctx, code=None, id=None):
|
|
620
|
-
"""Get entry by either its code or ID"""
|
|
621
|
-
|
|
622
|
-
if not code and not id:
|
|
623
|
-
raise click.UsageError("Please provide either an entry code or ID")
|
|
624
|
-
|
|
625
|
-
try:
|
|
626
|
-
# Ensure client and entry_manager are initialized
|
|
627
|
-
das_ctx.get_client()
|
|
628
|
-
# Pass client as a named parameter to avoid conflicts with 'id' parameter
|
|
629
|
-
entry = das_ctx.entry_manager.get(code=code, id=id)
|
|
630
|
-
if entry:
|
|
631
|
-
click.secho("Entry Details:", fg="green", bold=True)
|
|
632
|
-
click.echo("─" * 40)
|
|
633
|
-
for key, value in entry.items():
|
|
634
|
-
if key == 'Digital Object(s)' and isinstance(value, list):
|
|
635
|
-
continue
|
|
636
|
-
elif isinstance(value, list):
|
|
637
|
-
click.echo(f"{key}:")
|
|
638
|
-
for item in value:
|
|
639
|
-
click.echo(f" - {item}")
|
|
640
|
-
else:
|
|
641
|
-
click.echo(f"{key}: {value or ''}")
|
|
642
|
-
|
|
643
|
-
# if entry contains key: Digital Object(s) and its a list, print each object details
|
|
644
|
-
# with indentation for better readability
|
|
645
|
-
if 'Digital Object(s)' in entry and isinstance(entry['Digital Object(s)'], list):
|
|
646
|
-
click.echo()
|
|
647
|
-
click.echo("─" * 40)
|
|
648
|
-
click.echo("Digital Object(s):")
|
|
649
|
-
for obj in entry['Digital Object(s)']:
|
|
650
|
-
click.echo(f" - ID: {obj.get('Id')}")
|
|
651
|
-
click.echo(f" Name: {obj.get('Name')}")
|
|
652
|
-
click.echo(f" Type: {obj.get('Type')}")
|
|
653
|
-
click.echo(f" Links: {obj.get('Links')}")
|
|
654
|
-
click.echo()
|
|
655
|
-
click.echo("─" * 40)
|
|
656
|
-
else:
|
|
657
|
-
click.secho("Entry not found.", fg="yellow")
|
|
658
|
-
click.echo("Please check the entry code or ID.")
|
|
659
|
-
|
|
660
|
-
except Exception as e:
|
|
661
|
-
click.secho(f"Error: {e}", fg="red")
|
|
662
|
-
|
|
663
|
-
@entry.command("chown")
|
|
664
|
-
@click.option('--user', 'user_name', required=True, help='New owner username')
|
|
665
|
-
@click.option('--code', '-c', multiple=True, required=True, help='Entry code to transfer. Can be used multiple times.')
|
|
666
|
-
@pass_das_context
|
|
667
|
-
def chown_entries(das_ctx, user_name, code):
|
|
668
|
-
"""Change ownership of one or more entries by their codes.
|
|
669
|
-
|
|
670
|
-
Example:
|
|
671
|
-
|
|
672
|
-
\b
|
|
673
|
-
das entry chown --user alice --code ENT001 --code ENT002
|
|
674
|
-
"""
|
|
675
|
-
try:
|
|
676
|
-
# Ensure services are initialized
|
|
677
|
-
das_ctx.get_client()
|
|
678
|
-
|
|
679
|
-
entry_codes = list(code)
|
|
680
|
-
if not entry_codes:
|
|
681
|
-
raise click.UsageError("Please provide at least one --code")
|
|
682
|
-
|
|
683
|
-
result = das_ctx.entry_manager.chown(user_name=user_name, entry_code_list=entry_codes)
|
|
684
|
-
|
|
685
|
-
# If API returns a plain success or list, just report success
|
|
686
|
-
click.secho("✓ Ownership updated successfully!", fg="green")
|
|
687
|
-
if isinstance(result, dict):
|
|
688
|
-
# Show minimal feedback if available
|
|
689
|
-
updated = result.get('updated') or result.get('result') or result
|
|
690
|
-
if updated:
|
|
691
|
-
click.echo(json.dumps(updated, indent=2))
|
|
692
|
-
except Exception as e:
|
|
693
|
-
click.secho(f"Error: {e}", fg="red")
|
|
694
|
-
|
|
695
|
-
# Attribute commands group
|
|
696
|
-
@cli.group()
|
|
697
|
-
def attribute():
|
|
698
|
-
"""Commands for working with attributes"""
|
|
699
|
-
pass
|
|
700
|
-
|
|
701
|
-
# Cache commands group
|
|
702
|
-
@cli.group()
|
|
703
|
-
def cache():
|
|
704
|
-
"""Commands for working with cache"""
|
|
705
|
-
pass
|
|
706
|
-
|
|
707
|
-
# Config commands group
|
|
708
|
-
@cli.group()
|
|
709
|
-
def config():
|
|
710
|
-
"""Commands for configuring the CLI"""
|
|
711
|
-
pass
|
|
712
|
-
|
|
713
|
-
@cache.command("clear-all")
|
|
714
|
-
@pass_das_context
|
|
715
|
-
def clear_all_cache(das_ctx):
|
|
716
|
-
"""Clear all cache entries"""
|
|
717
|
-
client = das_ctx.get_client()
|
|
718
|
-
try:
|
|
719
|
-
result = client.cache.clear_all()
|
|
720
|
-
if result.get('success'):
|
|
721
|
-
click.secho("✓ All cache cleared!", fg="green")
|
|
722
|
-
else:
|
|
723
|
-
click.secho("Failed to clear cache.", fg="red")
|
|
724
|
-
except Exception as e:
|
|
725
|
-
click.secho(f"Error: {e}", fg="red")
|
|
726
|
-
|
|
727
|
-
@cache.command("list")
|
|
728
|
-
@pass_das_context
|
|
729
|
-
def list_cache(das_ctx):
|
|
730
|
-
"""List all cache entries"""
|
|
731
|
-
client = das_ctx.get_client()
|
|
732
|
-
try:
|
|
733
|
-
caches = client.cache.get_all()
|
|
734
|
-
if caches:
|
|
735
|
-
click.secho("Cache Entries:", fg="green", bold=True)
|
|
736
|
-
click.echo("─" * 40)
|
|
737
|
-
# Sort cache items by name before displaying
|
|
738
|
-
sorted_caches = sorted(caches.get('items', []), key=lambda x: x.get('name', '').lower())
|
|
739
|
-
for cache in sorted_caches:
|
|
740
|
-
click.echo(f"Name: {cache.get('name')}")
|
|
741
|
-
click.echo("─" * 40)
|
|
742
|
-
else:
|
|
743
|
-
click.secho("No cache entries found.", fg="yellow")
|
|
744
|
-
except Exception as e:
|
|
745
|
-
click.secho(f"Error: {e}", fg="red")
|
|
746
|
-
|
|
747
|
-
@cache.command("clear")
|
|
748
|
-
@click.argument('name', required=True)
|
|
749
|
-
@pass_das_context
|
|
750
|
-
def clear_cache(das_ctx, name):
|
|
751
|
-
"""Clear a specific cache by name"""
|
|
752
|
-
client = das_ctx.get_client()
|
|
753
|
-
try:
|
|
754
|
-
result = client.cache.clear_cache(name)
|
|
755
|
-
if result.get('success'):
|
|
756
|
-
click.secho(f"✓ Cache '{name}' cleared!", fg="green")
|
|
757
|
-
else:
|
|
758
|
-
click.secho(f"Failed to clear cache '{name}'.", fg="red")
|
|
759
|
-
except Exception as e:
|
|
760
|
-
click.secho(f"Error: {e}", fg="red")
|
|
761
|
-
|
|
762
|
-
@attribute.command("get")
|
|
763
|
-
@click.option('--id', type=int, default=None, help='Attribute ID')
|
|
764
|
-
@click.option('--name', default=None, help='Attribute name')
|
|
765
|
-
@click.option('--alias', default=None, help='Attribute alias')
|
|
766
|
-
@click.option('--table-name', default=None, help='Table name')
|
|
767
|
-
@pass_das_context
|
|
768
|
-
def get_attribute(das_ctx, id, name, alias, table_name):
|
|
769
|
-
"""Get attribute by ID, name, alias, or table name"""
|
|
770
|
-
if not any([id, name, alias, table_name]):
|
|
771
|
-
raise click.UsageError("Please provide at least one search parameter")
|
|
772
|
-
|
|
773
|
-
client = das_ctx.get_client()
|
|
774
|
-
try:
|
|
775
|
-
result = client.attributes.get_attribute(id=id, name=name, alias=alias, table_name=table_name)
|
|
776
|
-
if result.get('success') and result.get('result', {}).get('items'):
|
|
777
|
-
attributes = result['result']['items']
|
|
778
|
-
for attr in attributes:
|
|
779
|
-
click.secho("\nAttribute Details:", fg="green", bold=True)
|
|
780
|
-
click.echo("─" * 40)
|
|
781
|
-
click.echo(f"ID: {attr.get('id')}")
|
|
782
|
-
click.echo(f"Name: {attr.get('name')}")
|
|
783
|
-
click.echo(f"Description: {attr.get('description')}")
|
|
784
|
-
click.echo(f"Alias: {attr.get('alias')}")
|
|
785
|
-
click.echo(f"Table Name: {attr.get('tableName')}")
|
|
786
|
-
click.echo(f"Menu Name: {attr.get('menuName')}")
|
|
787
|
-
click.echo(f"Context: {attr.get('contextName')}")
|
|
788
|
-
click.echo(f"Indexable: {attr.get('isIndexable')}")
|
|
789
|
-
if attr.get('indexName'):
|
|
790
|
-
click.echo(f"Index Name: {attr.get('indexName')}")
|
|
791
|
-
click.echo("─" * 40)
|
|
792
|
-
else:
|
|
793
|
-
click.secho("No attributes found.", fg="yellow")
|
|
794
|
-
except Exception as e:
|
|
795
|
-
click.secho(f"Error: {e}", fg="red")
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
@attribute.command("get-name")
|
|
799
|
-
@click.argument('id', type=int, required=True)
|
|
800
|
-
@pass_das_context
|
|
801
|
-
def get_attribute_name(das_ctx, id):
|
|
802
|
-
"""Get attribute name by ID"""
|
|
803
|
-
client = das_ctx.get_client()
|
|
804
|
-
try:
|
|
805
|
-
name = client.attributes.get_name(id)
|
|
806
|
-
click.echo(name)
|
|
807
|
-
except Exception as e:
|
|
808
|
-
click.secho(f"Error: {e}", fg="red")
|
|
809
|
-
|
|
810
|
-
@attribute.command("get-id")
|
|
811
|
-
@click.argument('name', required=True)
|
|
812
|
-
@pass_das_context
|
|
813
|
-
def get_attribute_id(das_ctx, name):
|
|
814
|
-
"""Get attribute ID by name"""
|
|
815
|
-
client = das_ctx.get_client()
|
|
816
|
-
try:
|
|
817
|
-
attr_id = client.attributes.get_id(name)
|
|
818
|
-
click.echo(attr_id)
|
|
819
|
-
except Exception as e:
|
|
820
|
-
click.secho(f"Error: {e}", fg="red")
|
|
821
|
-
|
|
822
|
-
# SSL verification commands
|
|
823
|
-
@config.command("ssl-verify")
|
|
824
|
-
@click.argument('enabled', type=click.Choice(['true', 'false']), required=True)
|
|
825
|
-
def set_ssl_verify(enabled):
|
|
826
|
-
"""Set SSL certificate verification (true/false)"""
|
|
827
|
-
verify = enabled.lower() == 'true'
|
|
828
|
-
save_verify_ssl(verify)
|
|
829
|
-
status = "enabled" if verify else "disabled"
|
|
830
|
-
click.echo(f"SSL certificate verification {status}")
|
|
831
|
-
|
|
832
|
-
@config.command("ssl-status")
|
|
833
|
-
def get_ssl_status():
|
|
834
|
-
"""Show current SSL certificate verification status"""
|
|
835
|
-
status = "enabled" if VERIFY_SSL else "disabled"
|
|
836
|
-
click.echo(f"SSL certificate verification is currently {status}")
|
|
837
|
-
|
|
838
|
-
@config.command("reset")
|
|
839
|
-
@click.option('--force', is_flag=True, help='Skip confirmation prompt')
|
|
840
|
-
def reset_config(force):
|
|
841
|
-
"""Clear all configuration files (token, URL, and authentication info)"""
|
|
842
|
-
if not force:
|
|
843
|
-
if not click.confirm("This will remove all saved credentials and configuration. Are you sure?"):
|
|
844
|
-
click.echo("Operation cancelled.")
|
|
845
|
-
return
|
|
846
|
-
|
|
847
|
-
from das.common.config import clear_token, _config_dir
|
|
848
|
-
import shutil
|
|
849
|
-
|
|
850
|
-
# Clear token (handles both keyring and file-based storage)
|
|
851
|
-
clear_token()
|
|
852
|
-
|
|
853
|
-
# Get the config directory path
|
|
854
|
-
config_dir = _config_dir()
|
|
855
|
-
|
|
856
|
-
# Check if the directory exists
|
|
857
|
-
if config_dir.exists():
|
|
858
|
-
# Remove all files in the config directory
|
|
859
|
-
for file_path in config_dir.glob("*"):
|
|
860
|
-
if file_path.is_file():
|
|
861
|
-
try:
|
|
862
|
-
file_path.unlink()
|
|
863
|
-
click.echo(f"Removed: {file_path.name}")
|
|
864
|
-
except Exception as e:
|
|
865
|
-
click.secho(f"Failed to remove {file_path.name}: {e}", fg="red")
|
|
866
|
-
|
|
867
|
-
click.secho("✓ All configuration files and credentials have been removed.", fg="green")
|
|
868
|
-
|
|
869
|
-
# Download commands group
|
|
870
|
-
@cli.group()
|
|
871
|
-
def download():
|
|
872
|
-
"""Commands for working with downloads"""
|
|
873
|
-
pass
|
|
874
|
-
|
|
875
|
-
@download.command("request")
|
|
876
|
-
@click.option('--name', help='Name for the download request (defaults to timestamp if not provided)')
|
|
877
|
-
@click.option('--entry', '-e', multiple=True, required=True, help='Entry code to download files from. Can be used multiple times.')
|
|
878
|
-
@click.option('--file', '-f', multiple=True, help='File codes to download. If not specified, all files will be downloaded.')
|
|
879
|
-
@click.option('--from-file', help='Load download request from a JSON file')
|
|
880
|
-
@click.option('--format', 'output_format', default='table', type=click.Choice(['table', 'json']), help='Output format (table or json)')
|
|
881
|
-
@pass_das_context
|
|
882
|
-
def create_download_request(das_ctx, name, entry, file, from_file, output_format):
|
|
883
|
-
"""
|
|
884
|
-
Create a new download request for specified entries and files
|
|
885
|
-
|
|
886
|
-
Examples:
|
|
887
|
-
|
|
888
|
-
\b
|
|
889
|
-
# Download all files from an entry
|
|
890
|
-
das download request --entry ENT001
|
|
891
|
-
|
|
892
|
-
\b
|
|
893
|
-
# Download specific files from an entry
|
|
894
|
-
das download request --entry ENT001 --file FILE001 --file FILE002
|
|
895
|
-
|
|
896
|
-
\b
|
|
897
|
-
# Download from multiple entries
|
|
898
|
-
das download request --entry ENT001 --entry ENT002 --name "My Download"
|
|
899
|
-
|
|
900
|
-
\b
|
|
901
|
-
# Download specific files from multiple entries
|
|
902
|
-
das download request --entry ENT001 --file FILE001 --entry ENT002 --file FILE003
|
|
903
|
-
|
|
904
|
-
\b
|
|
905
|
-
# Download using a JSON file specification
|
|
906
|
-
das download request --from-file request.json
|
|
907
|
-
"""
|
|
908
|
-
try:
|
|
909
|
-
request_data = {}
|
|
910
|
-
|
|
911
|
-
# Handle name parameter
|
|
912
|
-
if name:
|
|
913
|
-
request_data['name'] = name
|
|
914
|
-
|
|
915
|
-
# Handle from-file parameter
|
|
916
|
-
if from_file:
|
|
917
|
-
click.echo(f"Loading download request from file: {from_file}")
|
|
918
|
-
try:
|
|
919
|
-
with open(from_file, 'r') as f:
|
|
920
|
-
file_data = json.load(f)
|
|
921
|
-
request_data.update(file_data)
|
|
922
|
-
except Exception as e:
|
|
923
|
-
raise click.UsageError(f"Failed to load file: {e}")
|
|
924
|
-
# Handle entry and file parameters
|
|
925
|
-
else:
|
|
926
|
-
# Group files by entry
|
|
927
|
-
current_entry = None
|
|
928
|
-
entry_files = {}
|
|
929
|
-
|
|
930
|
-
# If we have entries and files, we need to pair them correctly
|
|
931
|
-
if entry and file:
|
|
932
|
-
for arg in sys.argv:
|
|
933
|
-
if arg == '--entry' or arg == '-e':
|
|
934
|
-
# Next arg will be an entry code
|
|
935
|
-
current_entry = None
|
|
936
|
-
elif arg == '--file' or arg == '-f':
|
|
937
|
-
# Next arg will be a file code
|
|
938
|
-
pass
|
|
939
|
-
elif current_entry is None and arg in entry:
|
|
940
|
-
# This is an entry code
|
|
941
|
-
current_entry = arg
|
|
942
|
-
if current_entry not in entry_files:
|
|
943
|
-
entry_files[current_entry] = []
|
|
944
|
-
elif current_entry is not None and arg in file:
|
|
945
|
-
# This is a file code for the current entry
|
|
946
|
-
entry_files[current_entry].append(arg)
|
|
947
|
-
else:
|
|
948
|
-
# If we have entries but no files, download all files for each entry
|
|
949
|
-
for e in entry:
|
|
950
|
-
entry_files[e] = []
|
|
951
|
-
|
|
952
|
-
# Update request_data with the entry files
|
|
953
|
-
for e, files in entry_files.items():
|
|
954
|
-
request_data[e] = files
|
|
955
|
-
|
|
956
|
-
if not request_data or all(key == 'name' for key in request_data.keys()):
|
|
957
|
-
raise click.UsageError("No download request data provided")
|
|
958
|
-
|
|
959
|
-
# Execute the download request
|
|
960
|
-
# Make sure client and download_manager are initialized
|
|
961
|
-
das_ctx.get_client()
|
|
962
|
-
result = das_ctx.download_manager.create_download_request(request_data)
|
|
963
|
-
|
|
964
|
-
# Check if result contains errors
|
|
965
|
-
if isinstance(result, dict) and 'errors' in result:
|
|
966
|
-
click.secho("Download request failed with errors:", fg="red")
|
|
967
|
-
for error in result['errors']:
|
|
968
|
-
click.secho(f"- {error}", fg="red")
|
|
969
|
-
return
|
|
970
|
-
|
|
971
|
-
# Handle the result based on its type
|
|
972
|
-
if output_format == 'json':
|
|
973
|
-
# If result is a string (just the ID), wrap it in a dict for consistent JSON output
|
|
974
|
-
if isinstance(result, str):
|
|
975
|
-
click.echo(json.dumps({"id": result}, indent=2))
|
|
976
|
-
else:
|
|
977
|
-
click.echo(json.dumps(result, indent=2))
|
|
978
|
-
else:
|
|
979
|
-
click.secho("✓ Download request created successfully!", fg="green")
|
|
980
|
-
|
|
981
|
-
# If result is a string, it's just the request ID
|
|
982
|
-
if isinstance(result, str):
|
|
983
|
-
request_id = result
|
|
984
|
-
click.echo(f"Request ID: {request_id}")
|
|
985
|
-
|
|
986
|
-
# Show download instructions
|
|
987
|
-
click.echo("\nUse the following command to check the status of your download:")
|
|
988
|
-
click.secho(f" das download status {request_id}", fg="cyan")
|
|
989
|
-
else:
|
|
990
|
-
# Handle the case where result is a dictionary with more information
|
|
991
|
-
request_id = result.get('id', 'Unknown')
|
|
992
|
-
click.echo(f"Request ID: {request_id}")
|
|
993
|
-
click.echo(f"Status: {result.get('status', 'Pending')}")
|
|
994
|
-
|
|
995
|
-
# Display files in the request if available
|
|
996
|
-
if result.get('items'):
|
|
997
|
-
click.secho("\nFiles in this download request:", fg="blue")
|
|
998
|
-
|
|
999
|
-
headers = ["#", "File Name", "Entry", "Size", "Status"]
|
|
1000
|
-
table_data = []
|
|
1001
|
-
|
|
1002
|
-
for i, item in enumerate(result.get('items', []), 1):
|
|
1003
|
-
file_info = item.get('fileInfo', {})
|
|
1004
|
-
table_data.append([
|
|
1005
|
-
i,
|
|
1006
|
-
file_info.get('name', 'Unknown'),
|
|
1007
|
-
file_info.get('entryCode', 'Unknown'),
|
|
1008
|
-
file_info.get('size', 'Unknown'),
|
|
1009
|
-
item.get('status', 'Pending')
|
|
1010
|
-
])
|
|
1011
|
-
|
|
1012
|
-
click.echo(format_table(table_data, headers))
|
|
1013
|
-
|
|
1014
|
-
# Show download instructions
|
|
1015
|
-
click.echo("\nUse the following command to check the status of your download:")
|
|
1016
|
-
click.secho(f" das download status {request_id}", fg="cyan")
|
|
1017
|
-
|
|
1018
|
-
except Exception as e:
|
|
1019
|
-
click.secho(f"Error: {e}", fg="red")
|
|
1020
|
-
|
|
1021
|
-
@download.command("files")
|
|
1022
|
-
@click.argument('request_id', required=True)
|
|
1023
|
-
@click.option('--out', 'output_path', required=False, default='.', help='Output file path or directory (defaults to current directory)')
|
|
1024
|
-
@click.option('--force', is_flag=True, help='Overwrite existing file if present')
|
|
1025
|
-
@pass_das_context
|
|
1026
|
-
def download_files(das_ctx, request_id, output_path, force):
|
|
1027
|
-
"""
|
|
1028
|
-
Download the completed bundle for a download request and save it to disk.
|
|
1029
|
-
|
|
1030
|
-
Examples:
|
|
1031
|
-
|
|
1032
|
-
\b
|
|
1033
|
-
# Save into current directory with server-provided filename
|
|
1034
|
-
das download files 6b0e68e6-00cd-43a7-9c51-d56c9c091123
|
|
1035
|
-
|
|
1036
|
-
\b
|
|
1037
|
-
# Save to a specific folder
|
|
1038
|
-
das download files 6b0e68e6-00cd-43a7-9c51-d56c9c091123 --out C:\\Downloads
|
|
1039
|
-
|
|
1040
|
-
\b
|
|
1041
|
-
# Save to an explicit filename, overwriting if exists
|
|
1042
|
-
das download files 6b0e68e6-00cd-43a7-9c51-d56c9c091123 --out C:\\Downloads\\bundle.zip --force
|
|
1043
|
-
"""
|
|
1044
|
-
try:
|
|
1045
|
-
das_ctx.get_client()
|
|
1046
|
-
saved_path = das_ctx.download_manager.save_download(request_id=request_id, output_path=output_path, overwrite=force)
|
|
1047
|
-
click.secho(f"✓ Download saved to: {saved_path}", fg="green")
|
|
1048
|
-
except FileExistsError as e:
|
|
1049
|
-
click.secho(str(e), fg="yellow")
|
|
1050
|
-
except Exception as e:
|
|
1051
|
-
click.secho(f"Error: {e}", fg="red")
|
|
1052
|
-
|
|
1053
|
-
@download.command("delete-request")
|
|
1054
|
-
@click.argument('request_id', required=True)
|
|
1055
|
-
@pass_das_context
|
|
1056
|
-
def delete_download_request(das_ctx, request_id):
|
|
1057
|
-
"""
|
|
1058
|
-
Delete a download request by its ID
|
|
1059
|
-
|
|
1060
|
-
Example:
|
|
1061
|
-
|
|
1062
|
-
\b
|
|
1063
|
-
# Delete a download request
|
|
1064
|
-
das download delete-request 6b0e68e6-00cd-43a7-9c51-d56c9c091123
|
|
1065
|
-
"""
|
|
1066
|
-
try:
|
|
1067
|
-
# Ensure client and download_manager are initialized
|
|
1068
|
-
das_ctx.get_client()
|
|
1069
|
-
|
|
1070
|
-
# Call the delete_download_request method
|
|
1071
|
-
result = das_ctx.download_manager.delete_download_request(request_id)
|
|
1072
|
-
|
|
1073
|
-
# Display success message
|
|
1074
|
-
click.secho(f"✓ Download request '{request_id}' deleted successfully!", fg="green")
|
|
1075
|
-
|
|
1076
|
-
except Exception as e:
|
|
1077
|
-
click.secho(f"Error: {e}", fg="red")
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
@download.command("my-requests")
|
|
1081
|
-
@click.option('--format', 'output_format', default='table', type=click.Choice(['table', 'json']), help='Output format (table or json)')
|
|
1082
|
-
@pass_das_context
|
|
1083
|
-
def list_my_download_requests(das_ctx, output_format):
|
|
1084
|
-
"""List your download requests in a user-friendly format"""
|
|
1085
|
-
try:
|
|
1086
|
-
# Ensure services are initialized
|
|
1087
|
-
das_ctx.get_client()
|
|
1088
|
-
result = das_ctx.download_manager.get_my_requests()
|
|
1089
|
-
|
|
1090
|
-
# Normalize result shape
|
|
1091
|
-
total_count = 0
|
|
1092
|
-
items = []
|
|
1093
|
-
if isinstance(result, dict):
|
|
1094
|
-
total_count = result.get('totalCount', 0)
|
|
1095
|
-
items = result.get('items', [])
|
|
1096
|
-
elif isinstance(result, list):
|
|
1097
|
-
items = result
|
|
1098
|
-
total_count = len(items)
|
|
1099
|
-
|
|
1100
|
-
if output_format == 'json':
|
|
1101
|
-
payload = { 'totalCount': total_count, 'items': items }
|
|
1102
|
-
click.echo(json.dumps(payload, indent=2))
|
|
1103
|
-
return
|
|
1104
|
-
|
|
1105
|
-
if not items:
|
|
1106
|
-
click.secho("No download requests found.", fg="yellow")
|
|
1107
|
-
return
|
|
1108
|
-
|
|
1109
|
-
# Build a compact table
|
|
1110
|
-
headers = ["#", "ID", "Requester", "Created", "Status", "Files"]
|
|
1111
|
-
table_data = []
|
|
1112
|
-
|
|
1113
|
-
from das.common.enums import DownloadRequestStatus
|
|
1114
|
-
|
|
1115
|
-
def map_status(code):
|
|
1116
|
-
try:
|
|
1117
|
-
return DownloadRequestStatus(code).name.replace('_', ' ').title()
|
|
1118
|
-
except Exception:
|
|
1119
|
-
return str(code)
|
|
1120
|
-
|
|
1121
|
-
def fmt_dt(dt_str):
|
|
1122
|
-
if not isinstance(dt_str, str) or 'T' not in dt_str:
|
|
1123
|
-
return dt_str or ''
|
|
1124
|
-
date_part = dt_str.split('T')[0]
|
|
1125
|
-
time_part = dt_str.split('T')[1].split('.')[0]
|
|
1126
|
-
return f"{date_part} {time_part}"
|
|
1127
|
-
|
|
1128
|
-
for i, req in enumerate(items, 1):
|
|
1129
|
-
req_id = req.get('id', '')
|
|
1130
|
-
requester = req.get('requester', '')
|
|
1131
|
-
created = fmt_dt(req.get('createdOn'))
|
|
1132
|
-
status = map_status(req.get('status'))
|
|
1133
|
-
files = req.get('files') or []
|
|
1134
|
-
file_count = len(files)
|
|
1135
|
-
table_data.append([
|
|
1136
|
-
i,
|
|
1137
|
-
req_id[:8],
|
|
1138
|
-
requester.split(' - ')[0] if isinstance(requester, str) else requester,
|
|
1139
|
-
created,
|
|
1140
|
-
status,
|
|
1141
|
-
file_count
|
|
1142
|
-
])
|
|
1143
|
-
|
|
1144
|
-
click.secho(f"\nYour download requests ({total_count})", fg="blue")
|
|
1145
|
-
click.echo(format_table(table_data, headers))
|
|
1146
|
-
|
|
1147
|
-
# Show a brief file breakdown below the table
|
|
1148
|
-
click.secho("\nDetails:", fg="blue")
|
|
1149
|
-
for i, req in enumerate(items, 1):
|
|
1150
|
-
files = req.get('files') or []
|
|
1151
|
-
if not files:
|
|
1152
|
-
continue
|
|
1153
|
-
click.echo(f"{i}. {req.get('id', '')}")
|
|
1154
|
-
for f in files[:5]:
|
|
1155
|
-
# Map item status if present
|
|
1156
|
-
status_code = f.get('status')
|
|
1157
|
-
status_label = None
|
|
1158
|
-
try:
|
|
1159
|
-
from das.common.enums import DownloadRequestItemStatus
|
|
1160
|
-
status_label = DownloadRequestItemStatus(status_code).name.replace('_', ' ').title()
|
|
1161
|
-
except Exception:
|
|
1162
|
-
status_label = str(status_code)
|
|
1163
|
-
click.echo(f" - {f.get('fileName', f.get('needle', ''))} [{status_label}] ({f.get('digitalObjectType', '')})")
|
|
1164
|
-
if len(files) > 5:
|
|
1165
|
-
click.echo(f" ... and {len(files) - 5} more")
|
|
1166
|
-
|
|
1167
|
-
except Exception as e:
|
|
1168
|
-
click.secho(f"Error: {e}", fg="red")
|
|
1169
|
-
|
|
1170
|
-
# DAS AI commands group
|
|
1171
|
-
@cli.group()
|
|
1172
|
-
def ai():
|
|
1173
|
-
"""Commands for working with DAS AI"""
|
|
1174
|
-
pass
|
|
1175
|
-
|
|
1176
|
-
@ai.command("enable")
|
|
1177
|
-
@pass_das_context
|
|
1178
|
-
def enable_das_ai(das_ctx):
|
|
1179
|
-
"""Enable DAS AI interactive mode"""
|
|
1180
|
-
try:
|
|
1181
|
-
# Ensure OpenAI API key is configured
|
|
1182
|
-
api_key = os.getenv("OPENAI_API_KEY") or load_openai_api_key()
|
|
1183
|
-
if not api_key:
|
|
1184
|
-
click.secho("No OpenAI API key found.", fg="yellow")
|
|
1185
|
-
click.echo("You can set it via environment variable OPENAI_API_KEY, or save it now.")
|
|
1186
|
-
key = click.prompt("Enter your OpenAI API key", hide_input=True)
|
|
1187
|
-
if not key:
|
|
1188
|
-
raise click.UsageError("OpenAI API key is required to enable DAS AI.")
|
|
1189
|
-
save_openai_api_key(key)
|
|
1190
|
-
click.secho("✓ OpenAI API key saved securely.", fg="green")
|
|
1191
|
-
|
|
1192
|
-
# Get DAS AI instance
|
|
1193
|
-
das_ai = das_ctx.get_das_ai()
|
|
1194
|
-
|
|
1195
|
-
click.secho("🤖 DAS AI is now enabled!", fg="green", bold=True)
|
|
1196
|
-
click.echo("Starting interactive AI session...")
|
|
1197
|
-
click.echo("Type 'exit' to quit the AI session.")
|
|
1198
|
-
click.echo("=" * 50)
|
|
1199
|
-
|
|
1200
|
-
# Run the AI main loop
|
|
1201
|
-
import asyncio
|
|
1202
|
-
asyncio.run(das_ai.main())
|
|
1203
|
-
|
|
1204
|
-
except Exception as e:
|
|
1205
|
-
click.secho(f"Error enabling DAS AI: {e}", fg="red")
|
|
1206
|
-
click.echo("Make sure you have set your OPENAI_API_KEY environment variable.")
|
|
1207
|
-
|
|
1208
|
-
def _ai_clear_impl(force: bool):
|
|
1209
|
-
if not force:
|
|
1210
|
-
if not click.confirm("This will remove your saved OpenAI API key and authentication token. Continue?"):
|
|
1211
|
-
click.echo("Operation cancelled.")
|
|
1212
|
-
return
|
|
1213
|
-
try:
|
|
1214
|
-
clear_openai_api_key()
|
|
1215
|
-
clear_token()
|
|
1216
|
-
cfg_dir = _config_dir()
|
|
1217
|
-
removed_any = False
|
|
1218
|
-
for fname in ["openai_key.json", "token.json"]:
|
|
1219
|
-
p = cfg_dir / fname
|
|
1220
|
-
if p.exists() and p.is_file():
|
|
1221
|
-
try:
|
|
1222
|
-
p.unlink()
|
|
1223
|
-
removed_any = True
|
|
1224
|
-
except Exception:
|
|
1225
|
-
pass
|
|
1226
|
-
click.secho("✓ DAS AI credentials cleared.", fg="green")
|
|
1227
|
-
if removed_any:
|
|
1228
|
-
click.echo("Local credential files removed.")
|
|
1229
|
-
except Exception as e:
|
|
1230
|
-
click.secho(f"Error clearing credentials: {e}", fg="red")
|
|
1231
|
-
|
|
1232
|
-
@ai.command("clear")
|
|
1233
|
-
@click.option('--force', is_flag=True, help='Skip confirmation prompt')
|
|
1234
|
-
def ai_clear(force):
|
|
1235
|
-
"""Clear DAS AI credentials (OpenAI key) and auth token."""
|
|
1236
|
-
_ai_clear_impl(force)
|
|
1237
|
-
|
|
1238
|
-
@ai.command("logout")
|
|
1239
|
-
@click.option('--force', is_flag=True, help='Skip confirmation prompt')
|
|
1240
|
-
def ai_logout(force):
|
|
1241
|
-
"""Alias for 'das ai clear'"""
|
|
1242
|
-
_ai_clear_impl(force)
|
|
1243
|
-
|
|
1244
|
-
if __name__ == "__main__":
|
|
1245
|
-
cli()
|
|
1
|
+
import sys
|
|
2
|
+
import click
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from das.common.config import (
|
|
7
|
+
save_api_url, load_api_url, DEFAULT_BASE_URL,
|
|
8
|
+
save_verify_ssl, load_verify_ssl, VERIFY_SSL,
|
|
9
|
+
load_openai_api_key, save_openai_api_key, clear_openai_api_key,
|
|
10
|
+
clear_token, _config_dir
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from das.app import Das
|
|
14
|
+
from das.managers.download_manager import DownloadManager
|
|
15
|
+
from das.managers.entries_manager import EntryManager
|
|
16
|
+
from das.managers.search_manager import SearchManager
|
|
17
|
+
from das.managers.digital_objects_manager import DigitalObjectsManager
|
|
18
|
+
from das.common.file_utils import load_file_based_on_extension, parse_data_string
|
|
19
|
+
from das.ai.plugins.dasai import DasAI
|
|
20
|
+
|
|
21
|
+
# Simple table formatting function to avoid external dependencies
|
|
22
|
+
def format_table(data, headers):
|
|
23
|
+
"""Format data into a text-based table without external dependencies"""
|
|
24
|
+
if not data:
|
|
25
|
+
return "No data to display"
|
|
26
|
+
|
|
27
|
+
# Calculate column widths
|
|
28
|
+
col_widths = [max(len(str(h)), max([len(str(row[i])) for row in data] or [0])) for i, h in enumerate(headers)]
|
|
29
|
+
|
|
30
|
+
# Create the separator line
|
|
31
|
+
separator = '+' + '+'.join('-' * (w + 2) for w in col_widths) + '+'
|
|
32
|
+
|
|
33
|
+
# Create the header row
|
|
34
|
+
header_row = '|' + '|'.join(' ' + str(h).ljust(w) + ' ' for h, w in zip(headers, col_widths)) + '|'
|
|
35
|
+
|
|
36
|
+
# Create the data rows
|
|
37
|
+
data_rows = []
|
|
38
|
+
for row in data:
|
|
39
|
+
data_rows.append('|' + '|'.join(' ' + str(cell).ljust(w) + ' ' for cell, w in zip(row, col_widths)) + '|')
|
|
40
|
+
|
|
41
|
+
# Assemble the table
|
|
42
|
+
table = [separator, header_row, separator]
|
|
43
|
+
table.extend(data_rows)
|
|
44
|
+
table.append(separator)
|
|
45
|
+
|
|
46
|
+
return '\n'.join(table)
|
|
47
|
+
|
|
48
|
+
class DasCLI:
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self.client = None
|
|
51
|
+
self.api_url = None
|
|
52
|
+
self.entry_manager = None
|
|
53
|
+
self.search_manager = None
|
|
54
|
+
self.download_manager = None
|
|
55
|
+
self.digital_objects_manager = None
|
|
56
|
+
self.das_ai = None
|
|
57
|
+
|
|
58
|
+
def get_client(self):
|
|
59
|
+
"""Get DAS client instance, using saved API URL"""
|
|
60
|
+
if not self.api_url:
|
|
61
|
+
self.api_url = load_api_url() or DEFAULT_BASE_URL
|
|
62
|
+
if not self.api_url:
|
|
63
|
+
raise click.UsageError(
|
|
64
|
+
"No API URL configured. Please login first with 'das login --api-url <URL>'"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self.entry_manager = EntryManager()
|
|
68
|
+
self.search_manager = SearchManager()
|
|
69
|
+
self.download_manager = DownloadManager()
|
|
70
|
+
self.digital_objects_manager = DigitalObjectsManager()
|
|
71
|
+
# Set SSL verification based on saved config
|
|
72
|
+
if not self.client:
|
|
73
|
+
self.client = Das(self.api_url)
|
|
74
|
+
|
|
75
|
+
return self.client
|
|
76
|
+
|
|
77
|
+
def get_das_ai(self):
|
|
78
|
+
"""Get DAS AI instance"""
|
|
79
|
+
if not self.das_ai:
|
|
80
|
+
self.das_ai = DasAI()
|
|
81
|
+
return self.das_ai
|
|
82
|
+
|
|
83
|
+
pass_das_context = click.make_pass_decorator(DasCLI, ensure=True)
|
|
84
|
+
|
|
85
|
+
@click.group()
|
|
86
|
+
@click.version_option(package_name="das-cli")
|
|
87
|
+
@click.pass_context
|
|
88
|
+
def cli(ctx):
|
|
89
|
+
"""DAS Python CLI - Data Archive System client tool"""
|
|
90
|
+
ctx.obj = DasCLI()
|
|
91
|
+
|
|
92
|
+
@cli.command()
|
|
93
|
+
@click.option('--api-url', required=True, help='API base URL')
|
|
94
|
+
@click.option('--username', required=True, prompt=True, help='Username')
|
|
95
|
+
@click.option('--password', required=True, prompt=True, hide_input=True, help='Password')
|
|
96
|
+
@pass_das_context
|
|
97
|
+
def login(das_ctx, api_url, username, password):
|
|
98
|
+
"""Login and store authentication token"""
|
|
99
|
+
# Save API URL for future use
|
|
100
|
+
save_api_url(api_url)
|
|
101
|
+
das_ctx.api_url = api_url
|
|
102
|
+
|
|
103
|
+
# Authenticate
|
|
104
|
+
client = das_ctx.get_client()
|
|
105
|
+
token = client.authenticate(username, password)
|
|
106
|
+
if not token:
|
|
107
|
+
click.secho("❌ Authentication failed. Please check your credentials.", fg="red")
|
|
108
|
+
return
|
|
109
|
+
else:
|
|
110
|
+
click.secho("✓ Authentication successful!", fg="green")
|
|
111
|
+
|
|
112
|
+
# Search commands group
|
|
113
|
+
@cli.group()
|
|
114
|
+
def search():
|
|
115
|
+
"""Commands for searching entries"""
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
@search.command("help")
|
|
119
|
+
def search_help():
|
|
120
|
+
"""Show help about search query syntax and formats"""
|
|
121
|
+
click.secho("\nSearch Query Syntax Help", fg="green", bold=True)
|
|
122
|
+
click.echo("=" * 50)
|
|
123
|
+
|
|
124
|
+
click.secho("\nBasic Search:", fg="blue", bold=True)
|
|
125
|
+
click.echo(" das search entries --attribute <AttributeName> --query '<SearchQuery>'")
|
|
126
|
+
click.echo(" Example: das search entries --attribute Cores --query 'name(*64*)'")
|
|
127
|
+
|
|
128
|
+
click.secho("\nOutput Formats:", fg="blue", bold=True)
|
|
129
|
+
click.echo(" --format table Format results in a nice table (default)")
|
|
130
|
+
click.echo(" --format json Return results as JSON")
|
|
131
|
+
click.echo(" --format compact Simple one-line-per-result format")
|
|
132
|
+
click.echo(" --raw Show raw API response")
|
|
133
|
+
|
|
134
|
+
click.secho("\nQuery Syntax Examples:", fg="blue", bold=True)
|
|
135
|
+
click.echo(" 'name(*pattern*)' Search for pattern in name")
|
|
136
|
+
click.echo(" 'name(*pattern*);code(*ABC*)' Multiple conditions (AND)")
|
|
137
|
+
click.echo(" 'Create at(>2023-01-01)' Date comparison")
|
|
138
|
+
|
|
139
|
+
click.secho("\nGet Detailed Information:", fg="blue", bold=True)
|
|
140
|
+
click.echo(" das search entry <ID> Get detailed info for a single entry")
|
|
141
|
+
click.echo(" Example: das search entry 6b0e68e6-00cd-43a7-9c51-d56c9c091123")
|
|
142
|
+
|
|
143
|
+
click.secho("\nPagination:", fg="blue", bold=True)
|
|
144
|
+
click.echo(" --page <num> Show specific page of results")
|
|
145
|
+
click.echo(" --max-results <n> Number of results per page (default: 10)")
|
|
146
|
+
|
|
147
|
+
click.secho("\nSorting:", fg="blue", bold=True)
|
|
148
|
+
click.echo(" --sort-by <field> Field to sort by (default: Name)")
|
|
149
|
+
click.echo(" --sort-order <order> Sort order: 'asc' or 'desc' (default: asc)")
|
|
150
|
+
click.echo("\n")
|
|
151
|
+
|
|
152
|
+
@search.command("entries")
|
|
153
|
+
@click.option('--attribute', required=True, help='Attribute name to search in')
|
|
154
|
+
@click.option('--query', required=True, help='Search query string')
|
|
155
|
+
@click.option('--max-results', default=10, help='Maximum number of results to return')
|
|
156
|
+
@click.option('--page', default=1, help='Page number for paginated results')
|
|
157
|
+
@click.option('--sort-by', default='Name', help='Field to sort by (default: Name)')
|
|
158
|
+
@click.option('--sort-order', default='asc', type=click.Choice(['asc', 'desc']), help='Sort order (asc or desc)')
|
|
159
|
+
@click.option('--format', 'output_format', default='table', type=click.Choice(['table', 'json', 'compact']), help='Output format (table, json, or compact)')
|
|
160
|
+
@click.option('--raw', is_flag=True, help='Show raw response from API')
|
|
161
|
+
@pass_das_context
|
|
162
|
+
def search_entries(das_ctx, attribute, query, max_results, page, sort_by, sort_order, output_format, raw):
|
|
163
|
+
"""Search entries based on attribute and query"""
|
|
164
|
+
try:
|
|
165
|
+
# Ensure client and search_manager are initialized
|
|
166
|
+
das_ctx.get_client()
|
|
167
|
+
|
|
168
|
+
results = das_ctx.search_manager.search_entries(
|
|
169
|
+
attribute=attribute,
|
|
170
|
+
query=query,
|
|
171
|
+
max_results=max_results,
|
|
172
|
+
page=page,
|
|
173
|
+
sort_by=sort_by,
|
|
174
|
+
sort_order=sort_order
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if raw:
|
|
178
|
+
click.echo(results)
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
total_count = results.get('totalCount', 0)
|
|
182
|
+
items = results.get('items', [])
|
|
183
|
+
|
|
184
|
+
# Display search summary
|
|
185
|
+
click.secho(f"\nFound {total_count} results for query: '{query}' in attribute: '{attribute}'", fg="blue")
|
|
186
|
+
click.secho(f"Showing results {(page-1)*max_results + 1}-{min(page*max_results, total_count)} of {total_count}\n", fg="blue")
|
|
187
|
+
|
|
188
|
+
if not items:
|
|
189
|
+
click.secho("No results found.", fg="yellow")
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
if output_format == 'json':
|
|
193
|
+
click.echo(json.dumps(results, indent=2))
|
|
194
|
+
elif output_format == 'compact':
|
|
195
|
+
for i, item in enumerate(items, 1):
|
|
196
|
+
entry = item.get('entry', {})
|
|
197
|
+
code = entry.get('code', 'N/A')
|
|
198
|
+
name = entry.get('displayname', 'Unnamed')
|
|
199
|
+
description = entry.get('description', 'No description')
|
|
200
|
+
click.echo(f"{i}. {name} (Code: {code}) - {description}")
|
|
201
|
+
else: # table format
|
|
202
|
+
headers = ["#", "Code", "Name", "Description", "Owner"]
|
|
203
|
+
table_data = []
|
|
204
|
+
|
|
205
|
+
for i, item in enumerate(items, 1):
|
|
206
|
+
entry = item.get('entry', {})
|
|
207
|
+
table_data.append([
|
|
208
|
+
i,
|
|
209
|
+
entry.get('code', 'N/A'),
|
|
210
|
+
entry.get('displayname', 'Unnamed'),
|
|
211
|
+
entry.get('description', 'No description')[:50] + ('...' if entry.get('description', '') and len(entry.get('description', '')) > 50 else ''),
|
|
212
|
+
entry.get('owner', 'Unknown')
|
|
213
|
+
])
|
|
214
|
+
|
|
215
|
+
click.echo(format_table(table_data, headers))
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
click.secho(f"Error: {e}", fg="red")
|
|
219
|
+
|
|
220
|
+
# Hangfire commands group
|
|
221
|
+
@search.command("entry")
|
|
222
|
+
@click.argument('entry_id', required=True)
|
|
223
|
+
@click.option('--format', 'output_format', default='table', type=click.Choice(['table', 'json']), help='Output format (table or json)')
|
|
224
|
+
@pass_das_context
|
|
225
|
+
def get_entry(das_ctx, entry_id, output_format):
|
|
226
|
+
"""Get detailed information about a single entry by ID"""
|
|
227
|
+
try:
|
|
228
|
+
# Ensure client and entry_manager are initialized
|
|
229
|
+
das_ctx.get_client()
|
|
230
|
+
# Reusing the entry manager to get entry details by ID
|
|
231
|
+
entry = das_ctx.entry_manager.get_entry(entry_id)
|
|
232
|
+
|
|
233
|
+
if output_format == 'json':
|
|
234
|
+
click.echo(json.dumps(entry, indent=2))
|
|
235
|
+
else:
|
|
236
|
+
click.secho(f"\nEntry Details (ID: {entry_id})", fg="blue", bold=True)
|
|
237
|
+
click.echo("=" * 50)
|
|
238
|
+
|
|
239
|
+
if not entry or not isinstance(entry, dict):
|
|
240
|
+
click.secho("No entry found with the specified ID.", fg="yellow")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# Format display fields in a readable way
|
|
244
|
+
formatted_data = []
|
|
245
|
+
for key, value in sorted(entry.items()):
|
|
246
|
+
# Skip empty or None values
|
|
247
|
+
if value is None or value == '':
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
# Format date fields
|
|
251
|
+
if isinstance(value, str) and key.lower().endswith(('time', 'date')) and 'T' in value:
|
|
252
|
+
try:
|
|
253
|
+
date_part = value.split('T')[0]
|
|
254
|
+
time_part = value.split('T')[1].split('.')[0]
|
|
255
|
+
value = f"{date_part} {time_part}"
|
|
256
|
+
except:
|
|
257
|
+
pass # Keep original if formatting fails
|
|
258
|
+
|
|
259
|
+
# Add to formatted data
|
|
260
|
+
formatted_data.append((key, value))
|
|
261
|
+
|
|
262
|
+
# Display as key-value pairs
|
|
263
|
+
for key, value in formatted_data:
|
|
264
|
+
click.echo(f"{key.ljust(25)}: {value}")
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
click.secho(f"Error: {e}", fg="red")
|
|
268
|
+
|
|
269
|
+
@cli.group()
|
|
270
|
+
def hangfire():
|
|
271
|
+
"""Commands for working with Hangfire tasks"""
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
@hangfire.command("sync-doi")
|
|
275
|
+
@click.argument('id', required=True)
|
|
276
|
+
@pass_das_context
|
|
277
|
+
def sync_doi(das_ctx, id):
|
|
278
|
+
"""Trigger a DOI synchronization task by ID"""
|
|
279
|
+
client = das_ctx.get_client()
|
|
280
|
+
try:
|
|
281
|
+
client.hangfire.sync_doi(id)
|
|
282
|
+
click.secho(f"✓ DOI synchronization task triggered!", fg="green")
|
|
283
|
+
except Exception as e:
|
|
284
|
+
click.secho(f"Error: {e}", fg="red")
|
|
285
|
+
|
|
286
|
+
# Entries commands group
|
|
287
|
+
@cli.group()
|
|
288
|
+
def entry():
|
|
289
|
+
"""Commands for working with entries"""
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
@entry.command("update")
|
|
293
|
+
@click.option('--id', default=None, help='Entry ID to update (only used with --data for single entry)')
|
|
294
|
+
@click.option('--code', default=None, help='Entry code to update (only used with --data for single entry)')
|
|
295
|
+
@click.argument('file_path', required=False)
|
|
296
|
+
@click.option('--data', help='Data string in format { "key1": "value1", "key2": "value2", ... } or a list of such objects')
|
|
297
|
+
@pass_das_context
|
|
298
|
+
def update_entry(das_ctx, id, code, file_path, data):
|
|
299
|
+
"""Update entries from file or data string by ID or code.
|
|
300
|
+
|
|
301
|
+
For single updates, provide either --id or --code (or Id/Code in the data).
|
|
302
|
+
For bulk updates, each entry must contain a Code or Id field.
|
|
303
|
+
Files can contain multiple entries (rows in CSV/XLS or list of objects in JSON).
|
|
304
|
+
|
|
305
|
+
Examples:
|
|
306
|
+
|
|
307
|
+
\b
|
|
308
|
+
# Update by code from data string
|
|
309
|
+
das entry update --code ENT001 --data "{ 'Grant Public Access': 'Yes' }"
|
|
310
|
+
|
|
311
|
+
\b
|
|
312
|
+
# Update by ID from data string
|
|
313
|
+
das entry update --id 9b826efc-36f3-471c-ad18-9c1e00abe4fa --data "{ 'Name': 'Updated' }"
|
|
314
|
+
|
|
315
|
+
\b
|
|
316
|
+
# Update entries from JSON file (each object needs Code or Id)
|
|
317
|
+
das entry update c:\\data\\entries.json
|
|
318
|
+
|
|
319
|
+
\b
|
|
320
|
+
# Update entries from CSV file
|
|
321
|
+
das entry update c:\\data\\entries.csv
|
|
322
|
+
|
|
323
|
+
\b
|
|
324
|
+
# Update multiple entries from data string
|
|
325
|
+
das entry update --data "[{ 'Code': 'ENT001', ... }, { 'Code': 'ENT002', ... }]"
|
|
326
|
+
"""
|
|
327
|
+
try:
|
|
328
|
+
# Ensure client and entry_manager are initialized
|
|
329
|
+
das_ctx.get_client()
|
|
330
|
+
if not file_path and not data:
|
|
331
|
+
raise click.UsageError("Please provide either a file path or data string")
|
|
332
|
+
|
|
333
|
+
entry_data = None
|
|
334
|
+
is_bulk_update = False
|
|
335
|
+
|
|
336
|
+
if file_path:
|
|
337
|
+
click.echo(f"Loading data from file: {file_path}")
|
|
338
|
+
entry_data = load_file_based_on_extension(file_path)
|
|
339
|
+
|
|
340
|
+
# Check if we got a list or a single object
|
|
341
|
+
if isinstance(entry_data, list):
|
|
342
|
+
is_bulk_update = True
|
|
343
|
+
click.echo(f"Found {len(entry_data)} entries to update")
|
|
344
|
+
else:
|
|
345
|
+
# If we got a single object but no id/code was provided, check if it has Id or Code in data
|
|
346
|
+
if not id and not code:
|
|
347
|
+
data_id = next((entry_data.get(key) for key in entry_data if key.lower() == 'id'), None)
|
|
348
|
+
data_code = next((entry_data.get(key) for key in entry_data if key.lower() == 'code'), None)
|
|
349
|
+
if not data_id and not data_code:
|
|
350
|
+
raise click.UsageError("No --id or --code provided and entry data doesn't contain an Id or Code field")
|
|
351
|
+
id = id or data_id
|
|
352
|
+
code = code or data_code
|
|
353
|
+
|
|
354
|
+
elif data:
|
|
355
|
+
click.echo("Parsing data string")
|
|
356
|
+
entry_data = parse_data_string(data)
|
|
357
|
+
|
|
358
|
+
# Check if we got a list or a single object
|
|
359
|
+
if isinstance(entry_data, list):
|
|
360
|
+
is_bulk_update = True
|
|
361
|
+
click.echo(f"Found {len(entry_data)} entries to update")
|
|
362
|
+
elif not id and not code:
|
|
363
|
+
# If we got a single object but no id/code was provided, check if it has Id or Code in data
|
|
364
|
+
data_id = next((entry_data.get(key) for key in entry_data if key.lower() == 'id'), None)
|
|
365
|
+
data_code = next((entry_data.get(key) for key in entry_data if key.lower() == 'code'), None)
|
|
366
|
+
if not data_id and not data_code:
|
|
367
|
+
raise click.UsageError("No --id or --code provided and data doesn't contain an Id or Code field")
|
|
368
|
+
id = id or data_id
|
|
369
|
+
code = code or data_code
|
|
370
|
+
|
|
371
|
+
if not entry_data:
|
|
372
|
+
raise click.UsageError("No valid entry data found")
|
|
373
|
+
|
|
374
|
+
# For single entry update, require id or code
|
|
375
|
+
if not is_bulk_update and not id and not code:
|
|
376
|
+
raise click.UsageError("Please provide either --id or --code, or include Id or Code in the data")
|
|
377
|
+
|
|
378
|
+
# Update the entries
|
|
379
|
+
if is_bulk_update:
|
|
380
|
+
results = das_ctx.entry_manager.update(entries=entry_data)
|
|
381
|
+
|
|
382
|
+
# Display results
|
|
383
|
+
success_count = sum(1 for result in results if result.get('status') == 'success')
|
|
384
|
+
error_count = len(results) - success_count
|
|
385
|
+
|
|
386
|
+
if success_count > 0:
|
|
387
|
+
click.secho(f"✓ Successfully updated {success_count} entries", fg="green")
|
|
388
|
+
|
|
389
|
+
if error_count > 0:
|
|
390
|
+
click.secho(f"✗ Failed to update {error_count} entries", fg="red")
|
|
391
|
+
|
|
392
|
+
# Show details of the failures
|
|
393
|
+
click.echo("\nFailed updates:")
|
|
394
|
+
for result in results:
|
|
395
|
+
if result.get('status') == 'error':
|
|
396
|
+
ident = result.get('code') or result.get('id') or 'Unknown'
|
|
397
|
+
click.echo(f" {ident}: {result.get('error', 'Unknown error')}")
|
|
398
|
+
else:
|
|
399
|
+
# Single entry update
|
|
400
|
+
results = das_ctx.entry_manager.update(id=id, code=code, entry=entry_data)
|
|
401
|
+
|
|
402
|
+
if results and results[0].get('status') == 'success':
|
|
403
|
+
identifier = id or code
|
|
404
|
+
id_label = "ID" if id else "code"
|
|
405
|
+
click.secho(f"✓ Entry '{identifier}' ({id_label}) updated successfully!", fg="green")
|
|
406
|
+
click.echo(f"Entry ID: {results[0].get('id')}")
|
|
407
|
+
else:
|
|
408
|
+
error_msg = results[0].get('error', 'No response from server') if results else 'No response from server'
|
|
409
|
+
click.secho(f"Entry update failed: {error_msg}", fg="red")
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
click.secho(f"Error: {e}", fg="red")
|
|
413
|
+
|
|
414
|
+
@entry.command("delete")
|
|
415
|
+
@click.option('--code', default=None, help='Entry code')
|
|
416
|
+
@click.option('--id', default=None, help='Entry ID')
|
|
417
|
+
@click.option('--force', is_flag=True, help='Skip confirmation prompt')
|
|
418
|
+
@pass_das_context
|
|
419
|
+
def delete_entry(das_ctx, code, id, force):
|
|
420
|
+
"""Delete an entry by its code or ID"""
|
|
421
|
+
if not code and not id:
|
|
422
|
+
raise click.UsageError("Please provide either an entry code or ID")
|
|
423
|
+
|
|
424
|
+
# Ensure client and entry_manager are initialized
|
|
425
|
+
das_ctx.get_client()
|
|
426
|
+
|
|
427
|
+
identifier = code or id
|
|
428
|
+
id_label = "code" if code else "ID"
|
|
429
|
+
|
|
430
|
+
if not force:
|
|
431
|
+
if not click.confirm(f"Are you sure you want to delete entry with {id_label} '{identifier}'?"):
|
|
432
|
+
click.echo("Operation cancelled.")
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
das_ctx.entry_manager.delete(id=id, code=code)
|
|
437
|
+
click.secho(f"✓ Entry with {id_label} '{identifier}' deleted!", fg="green")
|
|
438
|
+
except Exception as e:
|
|
439
|
+
click.secho(f"Error: {e}", fg="red")
|
|
440
|
+
|
|
441
|
+
@entry.command("link-digital-objects")
|
|
442
|
+
@click.option('--entry-code', required=True, help='Entry code to link/unlink digital objects to')
|
|
443
|
+
@click.option('--digital-object-code', '-d', multiple=True, required=True, help='Digital object code. Use multiple times for multiple objects.')
|
|
444
|
+
@click.option('--unlink', is_flag=True, help='Unlink specified digital objects from the entry instead of linking')
|
|
445
|
+
@pass_das_context
|
|
446
|
+
def link_digital_objects(das_ctx, entry_code, digital_object_code, unlink):
|
|
447
|
+
"""Link or unlink existing digital objects to an entry by their codes.
|
|
448
|
+
|
|
449
|
+
Examples:
|
|
450
|
+
|
|
451
|
+
\b
|
|
452
|
+
# Link two digital objects to an entry
|
|
453
|
+
das entry link-digital-objects --entry-code ENT001 -d DO001 -d DO002
|
|
454
|
+
|
|
455
|
+
\b
|
|
456
|
+
# Unlink a digital object from an entry
|
|
457
|
+
das entry link-digital-objects --entry-code ENT001 -d DO003 --unlink
|
|
458
|
+
"""
|
|
459
|
+
try:
|
|
460
|
+
das_ctx.get_client()
|
|
461
|
+
codes = list(digital_object_code)
|
|
462
|
+
if not codes:
|
|
463
|
+
raise click.UsageError("Please provide at least one --digital-object-code")
|
|
464
|
+
|
|
465
|
+
success = das_ctx.digital_objects_manager.link_existing_digital_objects(
|
|
466
|
+
entry_code=entry_code,
|
|
467
|
+
digital_object_code_list=codes,
|
|
468
|
+
is_unlink=unlink,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
if success:
|
|
472
|
+
action = "unlinked" if unlink else "linked"
|
|
473
|
+
click.secho(f"✓ Successfully {action} {len(codes)} digital object(s) for entry '{entry_code}'", fg="green")
|
|
474
|
+
else:
|
|
475
|
+
click.secho("Operation did not report success.", fg="yellow")
|
|
476
|
+
except Exception as e:
|
|
477
|
+
click.secho(f"Error: {e}", fg="red")
|
|
478
|
+
|
|
479
|
+
@entry.command("create")
|
|
480
|
+
@click.option('--attribute', required=True, help='Attribute name')
|
|
481
|
+
@click.argument('file_path', required=False)
|
|
482
|
+
@click.option('--data', help='Data string in format { "key1": "value1", "key2": "value2", ... } or a list of such objects')
|
|
483
|
+
@pass_das_context
|
|
484
|
+
def create_entry(das_ctx, attribute, file_path=None, data=None):
|
|
485
|
+
"""Create one or more entries from file or data string
|
|
486
|
+
|
|
487
|
+
Files can contain multiple entries (rows in CSV/XLS or list of objects in JSON).
|
|
488
|
+
|
|
489
|
+
Examples:
|
|
490
|
+
|
|
491
|
+
\b
|
|
492
|
+
# Create entries from JSON file
|
|
493
|
+
das entry create --attribute core c:\\data\\entries.json
|
|
494
|
+
|
|
495
|
+
\b
|
|
496
|
+
# Create entries from CSV file
|
|
497
|
+
das entry create --attribute core c:\\data\\entries.csv
|
|
498
|
+
|
|
499
|
+
\b
|
|
500
|
+
# Create entries from Excel file
|
|
501
|
+
das entry create --attribute core c:\\data\\entries.xls
|
|
502
|
+
|
|
503
|
+
\b
|
|
504
|
+
# Create a single entry from data string
|
|
505
|
+
das entry create --attribute core --data { 'Grant Public Access': Yes, ... }
|
|
506
|
+
|
|
507
|
+
\b
|
|
508
|
+
# Create multiple entries from data string
|
|
509
|
+
das entry create --attribute core --data [{ 'Name': 'Entry 1', ... }, { 'Name': 'Entry 2', ... }]
|
|
510
|
+
"""
|
|
511
|
+
try:
|
|
512
|
+
# Ensure client and entry_manager are initialized
|
|
513
|
+
das_ctx.get_client()
|
|
514
|
+
if not file_path and not data:
|
|
515
|
+
raise click.UsageError("Please provide either a file path or data string")
|
|
516
|
+
|
|
517
|
+
entry_data = None
|
|
518
|
+
is_bulk_create = False
|
|
519
|
+
|
|
520
|
+
if file_path:
|
|
521
|
+
click.echo(f"Loading data from file: {file_path}")
|
|
522
|
+
entry_data = load_file_based_on_extension(file_path)
|
|
523
|
+
|
|
524
|
+
# Check if we got a list or a single object
|
|
525
|
+
if isinstance(entry_data, list):
|
|
526
|
+
is_bulk_create = True
|
|
527
|
+
click.echo(f"Found {len(entry_data)} entries to create")
|
|
528
|
+
|
|
529
|
+
elif data:
|
|
530
|
+
click.echo("Parsing data string")
|
|
531
|
+
entry_data = parse_data_string(data)
|
|
532
|
+
|
|
533
|
+
# Check if we got a list or a single object
|
|
534
|
+
if isinstance(entry_data, list):
|
|
535
|
+
is_bulk_create = True
|
|
536
|
+
click.echo(f"Found {len(entry_data)} entries to create")
|
|
537
|
+
|
|
538
|
+
if not entry_data:
|
|
539
|
+
raise click.UsageError("No valid entry data found")
|
|
540
|
+
|
|
541
|
+
# Create the entries
|
|
542
|
+
if is_bulk_create:
|
|
543
|
+
results = das_ctx.entry_manager.create(attribute=attribute, entries=entry_data)
|
|
544
|
+
|
|
545
|
+
# Display results
|
|
546
|
+
success_count = sum(1 for result in results if result.get('status') == 'success')
|
|
547
|
+
error_count = len(results) - success_count
|
|
548
|
+
|
|
549
|
+
if success_count > 0:
|
|
550
|
+
click.secho(f"✓ Successfully created {success_count} entries", fg="green")
|
|
551
|
+
|
|
552
|
+
# Show IDs of created entries
|
|
553
|
+
click.echo("\nCreated entry IDs:")
|
|
554
|
+
for result in results:
|
|
555
|
+
if result.get('status') == 'success':
|
|
556
|
+
click.echo(f" {result.get('id')}")
|
|
557
|
+
|
|
558
|
+
if error_count > 0:
|
|
559
|
+
click.secho(f"✗ Failed to create {error_count} entries", fg="red")
|
|
560
|
+
|
|
561
|
+
# Show details of the failures
|
|
562
|
+
click.echo("\nFailed creations:")
|
|
563
|
+
for i, result in enumerate(results):
|
|
564
|
+
if result.get('status') == 'error':
|
|
565
|
+
click.echo(f" Entry #{i+1}: {result.get('error', 'Unknown error')}")
|
|
566
|
+
else:
|
|
567
|
+
# Single entry creation
|
|
568
|
+
results = das_ctx.entry_manager.create(attribute=attribute, entry=entry_data)
|
|
569
|
+
|
|
570
|
+
if results and results[0].get('status') == 'success':
|
|
571
|
+
click.secho(f"✓ Entry created successfully!", fg="green")
|
|
572
|
+
click.echo(f"Entry ID: {results[0].get('id')}")
|
|
573
|
+
else:
|
|
574
|
+
error_msg = results[0].get('error', 'No ID returned') if results else 'No ID returned'
|
|
575
|
+
click.secho(f"Entry creation failed: {error_msg}", fg="red")
|
|
576
|
+
|
|
577
|
+
except Exception as e:
|
|
578
|
+
click.secho(f"Error: {e}", fg="red")
|
|
579
|
+
|
|
580
|
+
@entry.command("upload-digital-object")
|
|
581
|
+
@click.option('--entry-code', required=True, help='Entry code to attach the digital object to')
|
|
582
|
+
@click.option('--type', 'digital_object_type', required=True, help='Digital object type name (e.g., Dataset, File, Image)')
|
|
583
|
+
@click.option('--description', 'file_description', default='', help='Description for the uploaded file')
|
|
584
|
+
@click.argument('file_path', required=True)
|
|
585
|
+
@pass_das_context
|
|
586
|
+
def upload_digital_object(das_ctx, entry_code, digital_object_type, file_description, file_path):
|
|
587
|
+
"""Upload a file as a digital object and link it to an entry.
|
|
588
|
+
|
|
589
|
+
Examples:
|
|
590
|
+
|
|
591
|
+
\b
|
|
592
|
+
# Upload a dataset file and link to an entry
|
|
593
|
+
das entry upload-digital-object --entry-code ENT001 --type Dataset --description "CTD raw" c:\\data\\ctd.zip
|
|
594
|
+
"""
|
|
595
|
+
try:
|
|
596
|
+
# Ensure services are initialized
|
|
597
|
+
das_ctx.get_client()
|
|
598
|
+
|
|
599
|
+
# Perform upload and link
|
|
600
|
+
digital_object_id = das_ctx.digital_objects_manager.upload_digital_object(
|
|
601
|
+
entry_code=entry_code,
|
|
602
|
+
file_description=file_description,
|
|
603
|
+
digital_object_type=digital_object_type,
|
|
604
|
+
file_path=file_path,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if digital_object_id:
|
|
608
|
+
click.secho("✓ Digital object uploaded and linked successfully!", fg="green")
|
|
609
|
+
click.echo(f"Digital Object ID: {digital_object_id}")
|
|
610
|
+
else:
|
|
611
|
+
click.secho("Upload completed but no ID was returned.", fg="yellow")
|
|
612
|
+
except Exception as e:
|
|
613
|
+
click.secho(f"Error: {e}", fg="red")
|
|
614
|
+
|
|
615
|
+
@entry.command("get")
|
|
616
|
+
@click.option('--code', default=None, help='Entry code')
|
|
617
|
+
@click.option('--id', type=int, default=None, help='Entry ID')
|
|
618
|
+
@pass_das_context
|
|
619
|
+
def get_entry(das_ctx, code=None, id=None):
|
|
620
|
+
"""Get entry by either its code or ID"""
|
|
621
|
+
|
|
622
|
+
if not code and not id:
|
|
623
|
+
raise click.UsageError("Please provide either an entry code or ID")
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
# Ensure client and entry_manager are initialized
|
|
627
|
+
das_ctx.get_client()
|
|
628
|
+
# Pass client as a named parameter to avoid conflicts with 'id' parameter
|
|
629
|
+
entry = das_ctx.entry_manager.get(code=code, id=id)
|
|
630
|
+
if entry:
|
|
631
|
+
click.secho("Entry Details:", fg="green", bold=True)
|
|
632
|
+
click.echo("─" * 40)
|
|
633
|
+
for key, value in entry.items():
|
|
634
|
+
if key == 'Digital Object(s)' and isinstance(value, list):
|
|
635
|
+
continue
|
|
636
|
+
elif isinstance(value, list):
|
|
637
|
+
click.echo(f"{key}:")
|
|
638
|
+
for item in value:
|
|
639
|
+
click.echo(f" - {item}")
|
|
640
|
+
else:
|
|
641
|
+
click.echo(f"{key}: {value or ''}")
|
|
642
|
+
|
|
643
|
+
# if entry contains key: Digital Object(s) and its a list, print each object details
|
|
644
|
+
# with indentation for better readability
|
|
645
|
+
if 'Digital Object(s)' in entry and isinstance(entry['Digital Object(s)'], list):
|
|
646
|
+
click.echo()
|
|
647
|
+
click.echo("─" * 40)
|
|
648
|
+
click.echo("Digital Object(s):")
|
|
649
|
+
for obj in entry['Digital Object(s)']:
|
|
650
|
+
click.echo(f" - ID: {obj.get('Id')}")
|
|
651
|
+
click.echo(f" Name: {obj.get('Name')}")
|
|
652
|
+
click.echo(f" Type: {obj.get('Type')}")
|
|
653
|
+
click.echo(f" Links: {obj.get('Links')}")
|
|
654
|
+
click.echo()
|
|
655
|
+
click.echo("─" * 40)
|
|
656
|
+
else:
|
|
657
|
+
click.secho("Entry not found.", fg="yellow")
|
|
658
|
+
click.echo("Please check the entry code or ID.")
|
|
659
|
+
|
|
660
|
+
except Exception as e:
|
|
661
|
+
click.secho(f"Error: {e}", fg="red")
|
|
662
|
+
|
|
663
|
+
@entry.command("chown")
|
|
664
|
+
@click.option('--user', 'user_name', required=True, help='New owner username')
|
|
665
|
+
@click.option('--code', '-c', multiple=True, required=True, help='Entry code to transfer. Can be used multiple times.')
|
|
666
|
+
@pass_das_context
|
|
667
|
+
def chown_entries(das_ctx, user_name, code):
|
|
668
|
+
"""Change ownership of one or more entries by their codes.
|
|
669
|
+
|
|
670
|
+
Example:
|
|
671
|
+
|
|
672
|
+
\b
|
|
673
|
+
das entry chown --user alice --code ENT001 --code ENT002
|
|
674
|
+
"""
|
|
675
|
+
try:
|
|
676
|
+
# Ensure services are initialized
|
|
677
|
+
das_ctx.get_client()
|
|
678
|
+
|
|
679
|
+
entry_codes = list(code)
|
|
680
|
+
if not entry_codes:
|
|
681
|
+
raise click.UsageError("Please provide at least one --code")
|
|
682
|
+
|
|
683
|
+
result = das_ctx.entry_manager.chown(user_name=user_name, entry_code_list=entry_codes)
|
|
684
|
+
|
|
685
|
+
# If API returns a plain success or list, just report success
|
|
686
|
+
click.secho("✓ Ownership updated successfully!", fg="green")
|
|
687
|
+
if isinstance(result, dict):
|
|
688
|
+
# Show minimal feedback if available
|
|
689
|
+
updated = result.get('updated') or result.get('result') or result
|
|
690
|
+
if updated:
|
|
691
|
+
click.echo(json.dumps(updated, indent=2))
|
|
692
|
+
except Exception as e:
|
|
693
|
+
click.secho(f"Error: {e}", fg="red")
|
|
694
|
+
|
|
695
|
+
# Attribute commands group
|
|
696
|
+
@cli.group()
|
|
697
|
+
def attribute():
|
|
698
|
+
"""Commands for working with attributes"""
|
|
699
|
+
pass
|
|
700
|
+
|
|
701
|
+
# Cache commands group
|
|
702
|
+
@cli.group()
|
|
703
|
+
def cache():
|
|
704
|
+
"""Commands for working with cache"""
|
|
705
|
+
pass
|
|
706
|
+
|
|
707
|
+
# Config commands group
|
|
708
|
+
@cli.group()
|
|
709
|
+
def config():
|
|
710
|
+
"""Commands for configuring the CLI"""
|
|
711
|
+
pass
|
|
712
|
+
|
|
713
|
+
@cache.command("clear-all")
|
|
714
|
+
@pass_das_context
|
|
715
|
+
def clear_all_cache(das_ctx):
|
|
716
|
+
"""Clear all cache entries"""
|
|
717
|
+
client = das_ctx.get_client()
|
|
718
|
+
try:
|
|
719
|
+
result = client.cache.clear_all()
|
|
720
|
+
if result.get('success'):
|
|
721
|
+
click.secho("✓ All cache cleared!", fg="green")
|
|
722
|
+
else:
|
|
723
|
+
click.secho("Failed to clear cache.", fg="red")
|
|
724
|
+
except Exception as e:
|
|
725
|
+
click.secho(f"Error: {e}", fg="red")
|
|
726
|
+
|
|
727
|
+
@cache.command("list")
|
|
728
|
+
@pass_das_context
|
|
729
|
+
def list_cache(das_ctx):
|
|
730
|
+
"""List all cache entries"""
|
|
731
|
+
client = das_ctx.get_client()
|
|
732
|
+
try:
|
|
733
|
+
caches = client.cache.get_all()
|
|
734
|
+
if caches:
|
|
735
|
+
click.secho("Cache Entries:", fg="green", bold=True)
|
|
736
|
+
click.echo("─" * 40)
|
|
737
|
+
# Sort cache items by name before displaying
|
|
738
|
+
sorted_caches = sorted(caches.get('items', []), key=lambda x: x.get('name', '').lower())
|
|
739
|
+
for cache in sorted_caches:
|
|
740
|
+
click.echo(f"Name: {cache.get('name')}")
|
|
741
|
+
click.echo("─" * 40)
|
|
742
|
+
else:
|
|
743
|
+
click.secho("No cache entries found.", fg="yellow")
|
|
744
|
+
except Exception as e:
|
|
745
|
+
click.secho(f"Error: {e}", fg="red")
|
|
746
|
+
|
|
747
|
+
@cache.command("clear")
|
|
748
|
+
@click.argument('name', required=True)
|
|
749
|
+
@pass_das_context
|
|
750
|
+
def clear_cache(das_ctx, name):
|
|
751
|
+
"""Clear a specific cache by name"""
|
|
752
|
+
client = das_ctx.get_client()
|
|
753
|
+
try:
|
|
754
|
+
result = client.cache.clear_cache(name)
|
|
755
|
+
if result.get('success'):
|
|
756
|
+
click.secho(f"✓ Cache '{name}' cleared!", fg="green")
|
|
757
|
+
else:
|
|
758
|
+
click.secho(f"Failed to clear cache '{name}'.", fg="red")
|
|
759
|
+
except Exception as e:
|
|
760
|
+
click.secho(f"Error: {e}", fg="red")
|
|
761
|
+
|
|
762
|
+
@attribute.command("get")
|
|
763
|
+
@click.option('--id', type=int, default=None, help='Attribute ID')
|
|
764
|
+
@click.option('--name', default=None, help='Attribute name')
|
|
765
|
+
@click.option('--alias', default=None, help='Attribute alias')
|
|
766
|
+
@click.option('--table-name', default=None, help='Table name')
|
|
767
|
+
@pass_das_context
|
|
768
|
+
def get_attribute(das_ctx, id, name, alias, table_name):
|
|
769
|
+
"""Get attribute by ID, name, alias, or table name"""
|
|
770
|
+
if not any([id, name, alias, table_name]):
|
|
771
|
+
raise click.UsageError("Please provide at least one search parameter")
|
|
772
|
+
|
|
773
|
+
client = das_ctx.get_client()
|
|
774
|
+
try:
|
|
775
|
+
result = client.attributes.get_attribute(id=id, name=name, alias=alias, table_name=table_name)
|
|
776
|
+
if result.get('success') and result.get('result', {}).get('items'):
|
|
777
|
+
attributes = result['result']['items']
|
|
778
|
+
for attr in attributes:
|
|
779
|
+
click.secho("\nAttribute Details:", fg="green", bold=True)
|
|
780
|
+
click.echo("─" * 40)
|
|
781
|
+
click.echo(f"ID: {attr.get('id')}")
|
|
782
|
+
click.echo(f"Name: {attr.get('name')}")
|
|
783
|
+
click.echo(f"Description: {attr.get('description')}")
|
|
784
|
+
click.echo(f"Alias: {attr.get('alias')}")
|
|
785
|
+
click.echo(f"Table Name: {attr.get('tableName')}")
|
|
786
|
+
click.echo(f"Menu Name: {attr.get('menuName')}")
|
|
787
|
+
click.echo(f"Context: {attr.get('contextName')}")
|
|
788
|
+
click.echo(f"Indexable: {attr.get('isIndexable')}")
|
|
789
|
+
if attr.get('indexName'):
|
|
790
|
+
click.echo(f"Index Name: {attr.get('indexName')}")
|
|
791
|
+
click.echo("─" * 40)
|
|
792
|
+
else:
|
|
793
|
+
click.secho("No attributes found.", fg="yellow")
|
|
794
|
+
except Exception as e:
|
|
795
|
+
click.secho(f"Error: {e}", fg="red")
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
@attribute.command("get-name")
|
|
799
|
+
@click.argument('id', type=int, required=True)
|
|
800
|
+
@pass_das_context
|
|
801
|
+
def get_attribute_name(das_ctx, id):
|
|
802
|
+
"""Get attribute name by ID"""
|
|
803
|
+
client = das_ctx.get_client()
|
|
804
|
+
try:
|
|
805
|
+
name = client.attributes.get_name(id)
|
|
806
|
+
click.echo(name)
|
|
807
|
+
except Exception as e:
|
|
808
|
+
click.secho(f"Error: {e}", fg="red")
|
|
809
|
+
|
|
810
|
+
@attribute.command("get-id")
|
|
811
|
+
@click.argument('name', required=True)
|
|
812
|
+
@pass_das_context
|
|
813
|
+
def get_attribute_id(das_ctx, name):
|
|
814
|
+
"""Get attribute ID by name"""
|
|
815
|
+
client = das_ctx.get_client()
|
|
816
|
+
try:
|
|
817
|
+
attr_id = client.attributes.get_id(name)
|
|
818
|
+
click.echo(attr_id)
|
|
819
|
+
except Exception as e:
|
|
820
|
+
click.secho(f"Error: {e}", fg="red")
|
|
821
|
+
|
|
822
|
+
# SSL verification commands
|
|
823
|
+
@config.command("ssl-verify")
|
|
824
|
+
@click.argument('enabled', type=click.Choice(['true', 'false']), required=True)
|
|
825
|
+
def set_ssl_verify(enabled):
|
|
826
|
+
"""Set SSL certificate verification (true/false)"""
|
|
827
|
+
verify = enabled.lower() == 'true'
|
|
828
|
+
save_verify_ssl(verify)
|
|
829
|
+
status = "enabled" if verify else "disabled"
|
|
830
|
+
click.echo(f"SSL certificate verification {status}")
|
|
831
|
+
|
|
832
|
+
@config.command("ssl-status")
|
|
833
|
+
def get_ssl_status():
|
|
834
|
+
"""Show current SSL certificate verification status"""
|
|
835
|
+
status = "enabled" if VERIFY_SSL else "disabled"
|
|
836
|
+
click.echo(f"SSL certificate verification is currently {status}")
|
|
837
|
+
|
|
838
|
+
@config.command("reset")
|
|
839
|
+
@click.option('--force', is_flag=True, help='Skip confirmation prompt')
|
|
840
|
+
def reset_config(force):
|
|
841
|
+
"""Clear all configuration files (token, URL, and authentication info)"""
|
|
842
|
+
if not force:
|
|
843
|
+
if not click.confirm("This will remove all saved credentials and configuration. Are you sure?"):
|
|
844
|
+
click.echo("Operation cancelled.")
|
|
845
|
+
return
|
|
846
|
+
|
|
847
|
+
from das.common.config import clear_token, _config_dir
|
|
848
|
+
import shutil
|
|
849
|
+
|
|
850
|
+
# Clear token (handles both keyring and file-based storage)
|
|
851
|
+
clear_token()
|
|
852
|
+
|
|
853
|
+
# Get the config directory path
|
|
854
|
+
config_dir = _config_dir()
|
|
855
|
+
|
|
856
|
+
# Check if the directory exists
|
|
857
|
+
if config_dir.exists():
|
|
858
|
+
# Remove all files in the config directory
|
|
859
|
+
for file_path in config_dir.glob("*"):
|
|
860
|
+
if file_path.is_file():
|
|
861
|
+
try:
|
|
862
|
+
file_path.unlink()
|
|
863
|
+
click.echo(f"Removed: {file_path.name}")
|
|
864
|
+
except Exception as e:
|
|
865
|
+
click.secho(f"Failed to remove {file_path.name}: {e}", fg="red")
|
|
866
|
+
|
|
867
|
+
click.secho("✓ All configuration files and credentials have been removed.", fg="green")
|
|
868
|
+
|
|
869
|
+
# Download commands group
|
|
870
|
+
@cli.group()
|
|
871
|
+
def download():
|
|
872
|
+
"""Commands for working with downloads"""
|
|
873
|
+
pass
|
|
874
|
+
|
|
875
|
+
@download.command("request")
|
|
876
|
+
@click.option('--name', help='Name for the download request (defaults to timestamp if not provided)')
|
|
877
|
+
@click.option('--entry', '-e', multiple=True, required=True, help='Entry code to download files from. Can be used multiple times.')
|
|
878
|
+
@click.option('--file', '-f', multiple=True, help='File codes to download. If not specified, all files will be downloaded.')
|
|
879
|
+
@click.option('--from-file', help='Load download request from a JSON file')
|
|
880
|
+
@click.option('--format', 'output_format', default='table', type=click.Choice(['table', 'json']), help='Output format (table or json)')
|
|
881
|
+
@pass_das_context
|
|
882
|
+
def create_download_request(das_ctx, name, entry, file, from_file, output_format):
|
|
883
|
+
"""
|
|
884
|
+
Create a new download request for specified entries and files
|
|
885
|
+
|
|
886
|
+
Examples:
|
|
887
|
+
|
|
888
|
+
\b
|
|
889
|
+
# Download all files from an entry
|
|
890
|
+
das download request --entry ENT001
|
|
891
|
+
|
|
892
|
+
\b
|
|
893
|
+
# Download specific files from an entry
|
|
894
|
+
das download request --entry ENT001 --file FILE001 --file FILE002
|
|
895
|
+
|
|
896
|
+
\b
|
|
897
|
+
# Download from multiple entries
|
|
898
|
+
das download request --entry ENT001 --entry ENT002 --name "My Download"
|
|
899
|
+
|
|
900
|
+
\b
|
|
901
|
+
# Download specific files from multiple entries
|
|
902
|
+
das download request --entry ENT001 --file FILE001 --entry ENT002 --file FILE003
|
|
903
|
+
|
|
904
|
+
\b
|
|
905
|
+
# Download using a JSON file specification
|
|
906
|
+
das download request --from-file request.json
|
|
907
|
+
"""
|
|
908
|
+
try:
|
|
909
|
+
request_data = {}
|
|
910
|
+
|
|
911
|
+
# Handle name parameter
|
|
912
|
+
if name:
|
|
913
|
+
request_data['name'] = name
|
|
914
|
+
|
|
915
|
+
# Handle from-file parameter
|
|
916
|
+
if from_file:
|
|
917
|
+
click.echo(f"Loading download request from file: {from_file}")
|
|
918
|
+
try:
|
|
919
|
+
with open(from_file, 'r') as f:
|
|
920
|
+
file_data = json.load(f)
|
|
921
|
+
request_data.update(file_data)
|
|
922
|
+
except Exception as e:
|
|
923
|
+
raise click.UsageError(f"Failed to load file: {e}")
|
|
924
|
+
# Handle entry and file parameters
|
|
925
|
+
else:
|
|
926
|
+
# Group files by entry
|
|
927
|
+
current_entry = None
|
|
928
|
+
entry_files = {}
|
|
929
|
+
|
|
930
|
+
# If we have entries and files, we need to pair them correctly
|
|
931
|
+
if entry and file:
|
|
932
|
+
for arg in sys.argv:
|
|
933
|
+
if arg == '--entry' or arg == '-e':
|
|
934
|
+
# Next arg will be an entry code
|
|
935
|
+
current_entry = None
|
|
936
|
+
elif arg == '--file' or arg == '-f':
|
|
937
|
+
# Next arg will be a file code
|
|
938
|
+
pass
|
|
939
|
+
elif current_entry is None and arg in entry:
|
|
940
|
+
# This is an entry code
|
|
941
|
+
current_entry = arg
|
|
942
|
+
if current_entry not in entry_files:
|
|
943
|
+
entry_files[current_entry] = []
|
|
944
|
+
elif current_entry is not None and arg in file:
|
|
945
|
+
# This is a file code for the current entry
|
|
946
|
+
entry_files[current_entry].append(arg)
|
|
947
|
+
else:
|
|
948
|
+
# If we have entries but no files, download all files for each entry
|
|
949
|
+
for e in entry:
|
|
950
|
+
entry_files[e] = []
|
|
951
|
+
|
|
952
|
+
# Update request_data with the entry files
|
|
953
|
+
for e, files in entry_files.items():
|
|
954
|
+
request_data[e] = files
|
|
955
|
+
|
|
956
|
+
if not request_data or all(key == 'name' for key in request_data.keys()):
|
|
957
|
+
raise click.UsageError("No download request data provided")
|
|
958
|
+
|
|
959
|
+
# Execute the download request
|
|
960
|
+
# Make sure client and download_manager are initialized
|
|
961
|
+
das_ctx.get_client()
|
|
962
|
+
result = das_ctx.download_manager.create_download_request(request_data)
|
|
963
|
+
|
|
964
|
+
# Check if result contains errors
|
|
965
|
+
if isinstance(result, dict) and 'errors' in result:
|
|
966
|
+
click.secho("Download request failed with errors:", fg="red")
|
|
967
|
+
for error in result['errors']:
|
|
968
|
+
click.secho(f"- {error}", fg="red")
|
|
969
|
+
return
|
|
970
|
+
|
|
971
|
+
# Handle the result based on its type
|
|
972
|
+
if output_format == 'json':
|
|
973
|
+
# If result is a string (just the ID), wrap it in a dict for consistent JSON output
|
|
974
|
+
if isinstance(result, str):
|
|
975
|
+
click.echo(json.dumps({"id": result}, indent=2))
|
|
976
|
+
else:
|
|
977
|
+
click.echo(json.dumps(result, indent=2))
|
|
978
|
+
else:
|
|
979
|
+
click.secho("✓ Download request created successfully!", fg="green")
|
|
980
|
+
|
|
981
|
+
# If result is a string, it's just the request ID
|
|
982
|
+
if isinstance(result, str):
|
|
983
|
+
request_id = result
|
|
984
|
+
click.echo(f"Request ID: {request_id}")
|
|
985
|
+
|
|
986
|
+
# Show download instructions
|
|
987
|
+
click.echo("\nUse the following command to check the status of your download:")
|
|
988
|
+
click.secho(f" das download status {request_id}", fg="cyan")
|
|
989
|
+
else:
|
|
990
|
+
# Handle the case where result is a dictionary with more information
|
|
991
|
+
request_id = result.get('id', 'Unknown')
|
|
992
|
+
click.echo(f"Request ID: {request_id}")
|
|
993
|
+
click.echo(f"Status: {result.get('status', 'Pending')}")
|
|
994
|
+
|
|
995
|
+
# Display files in the request if available
|
|
996
|
+
if result.get('items'):
|
|
997
|
+
click.secho("\nFiles in this download request:", fg="blue")
|
|
998
|
+
|
|
999
|
+
headers = ["#", "File Name", "Entry", "Size", "Status"]
|
|
1000
|
+
table_data = []
|
|
1001
|
+
|
|
1002
|
+
for i, item in enumerate(result.get('items', []), 1):
|
|
1003
|
+
file_info = item.get('fileInfo', {})
|
|
1004
|
+
table_data.append([
|
|
1005
|
+
i,
|
|
1006
|
+
file_info.get('name', 'Unknown'),
|
|
1007
|
+
file_info.get('entryCode', 'Unknown'),
|
|
1008
|
+
file_info.get('size', 'Unknown'),
|
|
1009
|
+
item.get('status', 'Pending')
|
|
1010
|
+
])
|
|
1011
|
+
|
|
1012
|
+
click.echo(format_table(table_data, headers))
|
|
1013
|
+
|
|
1014
|
+
# Show download instructions
|
|
1015
|
+
click.echo("\nUse the following command to check the status of your download:")
|
|
1016
|
+
click.secho(f" das download status {request_id}", fg="cyan")
|
|
1017
|
+
|
|
1018
|
+
except Exception as e:
|
|
1019
|
+
click.secho(f"Error: {e}", fg="red")
|
|
1020
|
+
|
|
1021
|
+
@download.command("files")
|
|
1022
|
+
@click.argument('request_id', required=True)
|
|
1023
|
+
@click.option('--out', 'output_path', required=False, default='.', help='Output file path or directory (defaults to current directory)')
|
|
1024
|
+
@click.option('--force', is_flag=True, help='Overwrite existing file if present')
|
|
1025
|
+
@pass_das_context
|
|
1026
|
+
def download_files(das_ctx, request_id, output_path, force):
|
|
1027
|
+
"""
|
|
1028
|
+
Download the completed bundle for a download request and save it to disk.
|
|
1029
|
+
|
|
1030
|
+
Examples:
|
|
1031
|
+
|
|
1032
|
+
\b
|
|
1033
|
+
# Save into current directory with server-provided filename
|
|
1034
|
+
das download files 6b0e68e6-00cd-43a7-9c51-d56c9c091123
|
|
1035
|
+
|
|
1036
|
+
\b
|
|
1037
|
+
# Save to a specific folder
|
|
1038
|
+
das download files 6b0e68e6-00cd-43a7-9c51-d56c9c091123 --out C:\\Downloads
|
|
1039
|
+
|
|
1040
|
+
\b
|
|
1041
|
+
# Save to an explicit filename, overwriting if exists
|
|
1042
|
+
das download files 6b0e68e6-00cd-43a7-9c51-d56c9c091123 --out C:\\Downloads\\bundle.zip --force
|
|
1043
|
+
"""
|
|
1044
|
+
try:
|
|
1045
|
+
das_ctx.get_client()
|
|
1046
|
+
saved_path = das_ctx.download_manager.save_download(request_id=request_id, output_path=output_path, overwrite=force)
|
|
1047
|
+
click.secho(f"✓ Download saved to: {saved_path}", fg="green")
|
|
1048
|
+
except FileExistsError as e:
|
|
1049
|
+
click.secho(str(e), fg="yellow")
|
|
1050
|
+
except Exception as e:
|
|
1051
|
+
click.secho(f"Error: {e}", fg="red")
|
|
1052
|
+
|
|
1053
|
+
@download.command("delete-request")
|
|
1054
|
+
@click.argument('request_id', required=True)
|
|
1055
|
+
@pass_das_context
|
|
1056
|
+
def delete_download_request(das_ctx, request_id):
|
|
1057
|
+
"""
|
|
1058
|
+
Delete a download request by its ID
|
|
1059
|
+
|
|
1060
|
+
Example:
|
|
1061
|
+
|
|
1062
|
+
\b
|
|
1063
|
+
# Delete a download request
|
|
1064
|
+
das download delete-request 6b0e68e6-00cd-43a7-9c51-d56c9c091123
|
|
1065
|
+
"""
|
|
1066
|
+
try:
|
|
1067
|
+
# Ensure client and download_manager are initialized
|
|
1068
|
+
das_ctx.get_client()
|
|
1069
|
+
|
|
1070
|
+
# Call the delete_download_request method
|
|
1071
|
+
result = das_ctx.download_manager.delete_download_request(request_id)
|
|
1072
|
+
|
|
1073
|
+
# Display success message
|
|
1074
|
+
click.secho(f"✓ Download request '{request_id}' deleted successfully!", fg="green")
|
|
1075
|
+
|
|
1076
|
+
except Exception as e:
|
|
1077
|
+
click.secho(f"Error: {e}", fg="red")
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
@download.command("my-requests")
|
|
1081
|
+
@click.option('--format', 'output_format', default='table', type=click.Choice(['table', 'json']), help='Output format (table or json)')
|
|
1082
|
+
@pass_das_context
|
|
1083
|
+
def list_my_download_requests(das_ctx, output_format):
|
|
1084
|
+
"""List your download requests in a user-friendly format"""
|
|
1085
|
+
try:
|
|
1086
|
+
# Ensure services are initialized
|
|
1087
|
+
das_ctx.get_client()
|
|
1088
|
+
result = das_ctx.download_manager.get_my_requests()
|
|
1089
|
+
|
|
1090
|
+
# Normalize result shape
|
|
1091
|
+
total_count = 0
|
|
1092
|
+
items = []
|
|
1093
|
+
if isinstance(result, dict):
|
|
1094
|
+
total_count = result.get('totalCount', 0)
|
|
1095
|
+
items = result.get('items', [])
|
|
1096
|
+
elif isinstance(result, list):
|
|
1097
|
+
items = result
|
|
1098
|
+
total_count = len(items)
|
|
1099
|
+
|
|
1100
|
+
if output_format == 'json':
|
|
1101
|
+
payload = { 'totalCount': total_count, 'items': items }
|
|
1102
|
+
click.echo(json.dumps(payload, indent=2))
|
|
1103
|
+
return
|
|
1104
|
+
|
|
1105
|
+
if not items:
|
|
1106
|
+
click.secho("No download requests found.", fg="yellow")
|
|
1107
|
+
return
|
|
1108
|
+
|
|
1109
|
+
# Build a compact table
|
|
1110
|
+
headers = ["#", "ID", "Requester", "Created", "Status", "Files"]
|
|
1111
|
+
table_data = []
|
|
1112
|
+
|
|
1113
|
+
from das.common.enums import DownloadRequestStatus
|
|
1114
|
+
|
|
1115
|
+
def map_status(code):
|
|
1116
|
+
try:
|
|
1117
|
+
return DownloadRequestStatus(code).name.replace('_', ' ').title()
|
|
1118
|
+
except Exception:
|
|
1119
|
+
return str(code)
|
|
1120
|
+
|
|
1121
|
+
def fmt_dt(dt_str):
|
|
1122
|
+
if not isinstance(dt_str, str) or 'T' not in dt_str:
|
|
1123
|
+
return dt_str or ''
|
|
1124
|
+
date_part = dt_str.split('T')[0]
|
|
1125
|
+
time_part = dt_str.split('T')[1].split('.')[0]
|
|
1126
|
+
return f"{date_part} {time_part}"
|
|
1127
|
+
|
|
1128
|
+
for i, req in enumerate(items, 1):
|
|
1129
|
+
req_id = req.get('id', '')
|
|
1130
|
+
requester = req.get('requester', '')
|
|
1131
|
+
created = fmt_dt(req.get('createdOn'))
|
|
1132
|
+
status = map_status(req.get('status'))
|
|
1133
|
+
files = req.get('files') or []
|
|
1134
|
+
file_count = len(files)
|
|
1135
|
+
table_data.append([
|
|
1136
|
+
i,
|
|
1137
|
+
req_id[:8],
|
|
1138
|
+
requester.split(' - ')[0] if isinstance(requester, str) else requester,
|
|
1139
|
+
created,
|
|
1140
|
+
status,
|
|
1141
|
+
file_count
|
|
1142
|
+
])
|
|
1143
|
+
|
|
1144
|
+
click.secho(f"\nYour download requests ({total_count})", fg="blue")
|
|
1145
|
+
click.echo(format_table(table_data, headers))
|
|
1146
|
+
|
|
1147
|
+
# Show a brief file breakdown below the table
|
|
1148
|
+
click.secho("\nDetails:", fg="blue")
|
|
1149
|
+
for i, req in enumerate(items, 1):
|
|
1150
|
+
files = req.get('files') or []
|
|
1151
|
+
if not files:
|
|
1152
|
+
continue
|
|
1153
|
+
click.echo(f"{i}. {req.get('id', '')}")
|
|
1154
|
+
for f in files[:5]:
|
|
1155
|
+
# Map item status if present
|
|
1156
|
+
status_code = f.get('status')
|
|
1157
|
+
status_label = None
|
|
1158
|
+
try:
|
|
1159
|
+
from das.common.enums import DownloadRequestItemStatus
|
|
1160
|
+
status_label = DownloadRequestItemStatus(status_code).name.replace('_', ' ').title()
|
|
1161
|
+
except Exception:
|
|
1162
|
+
status_label = str(status_code)
|
|
1163
|
+
click.echo(f" - {f.get('fileName', f.get('needle', ''))} [{status_label}] ({f.get('digitalObjectType', '')})")
|
|
1164
|
+
if len(files) > 5:
|
|
1165
|
+
click.echo(f" ... and {len(files) - 5} more")
|
|
1166
|
+
|
|
1167
|
+
except Exception as e:
|
|
1168
|
+
click.secho(f"Error: {e}", fg="red")
|
|
1169
|
+
|
|
1170
|
+
# DAS AI commands group
|
|
1171
|
+
@cli.group()
|
|
1172
|
+
def ai():
|
|
1173
|
+
"""Commands for working with DAS AI"""
|
|
1174
|
+
pass
|
|
1175
|
+
|
|
1176
|
+
@ai.command("enable")
|
|
1177
|
+
@pass_das_context
|
|
1178
|
+
def enable_das_ai(das_ctx):
|
|
1179
|
+
"""Enable DAS AI interactive mode"""
|
|
1180
|
+
try:
|
|
1181
|
+
# Ensure OpenAI API key is configured
|
|
1182
|
+
api_key = os.getenv("OPENAI_API_KEY") or load_openai_api_key()
|
|
1183
|
+
if not api_key:
|
|
1184
|
+
click.secho("No OpenAI API key found.", fg="yellow")
|
|
1185
|
+
click.echo("You can set it via environment variable OPENAI_API_KEY, or save it now.")
|
|
1186
|
+
key = click.prompt("Enter your OpenAI API key", hide_input=True)
|
|
1187
|
+
if not key:
|
|
1188
|
+
raise click.UsageError("OpenAI API key is required to enable DAS AI.")
|
|
1189
|
+
save_openai_api_key(key)
|
|
1190
|
+
click.secho("✓ OpenAI API key saved securely.", fg="green")
|
|
1191
|
+
|
|
1192
|
+
# Get DAS AI instance
|
|
1193
|
+
das_ai = das_ctx.get_das_ai()
|
|
1194
|
+
|
|
1195
|
+
click.secho("🤖 DAS AI is now enabled!", fg="green", bold=True)
|
|
1196
|
+
click.echo("Starting interactive AI session...")
|
|
1197
|
+
click.echo("Type 'exit' to quit the AI session.")
|
|
1198
|
+
click.echo("=" * 50)
|
|
1199
|
+
|
|
1200
|
+
# Run the AI main loop
|
|
1201
|
+
import asyncio
|
|
1202
|
+
asyncio.run(das_ai.main())
|
|
1203
|
+
|
|
1204
|
+
except Exception as e:
|
|
1205
|
+
click.secho(f"Error enabling DAS AI: {e}", fg="red")
|
|
1206
|
+
click.echo("Make sure you have set your OPENAI_API_KEY environment variable.")
|
|
1207
|
+
|
|
1208
|
+
def _ai_clear_impl(force: bool):
|
|
1209
|
+
if not force:
|
|
1210
|
+
if not click.confirm("This will remove your saved OpenAI API key and authentication token. Continue?"):
|
|
1211
|
+
click.echo("Operation cancelled.")
|
|
1212
|
+
return
|
|
1213
|
+
try:
|
|
1214
|
+
clear_openai_api_key()
|
|
1215
|
+
clear_token()
|
|
1216
|
+
cfg_dir = _config_dir()
|
|
1217
|
+
removed_any = False
|
|
1218
|
+
for fname in ["openai_key.json", "token.json"]:
|
|
1219
|
+
p = cfg_dir / fname
|
|
1220
|
+
if p.exists() and p.is_file():
|
|
1221
|
+
try:
|
|
1222
|
+
p.unlink()
|
|
1223
|
+
removed_any = True
|
|
1224
|
+
except Exception:
|
|
1225
|
+
pass
|
|
1226
|
+
click.secho("✓ DAS AI credentials cleared.", fg="green")
|
|
1227
|
+
if removed_any:
|
|
1228
|
+
click.echo("Local credential files removed.")
|
|
1229
|
+
except Exception as e:
|
|
1230
|
+
click.secho(f"Error clearing credentials: {e}", fg="red")
|
|
1231
|
+
|
|
1232
|
+
@ai.command("clear")
|
|
1233
|
+
@click.option('--force', is_flag=True, help='Skip confirmation prompt')
|
|
1234
|
+
def ai_clear(force):
|
|
1235
|
+
"""Clear DAS AI credentials (OpenAI key) and auth token."""
|
|
1236
|
+
_ai_clear_impl(force)
|
|
1237
|
+
|
|
1238
|
+
@ai.command("logout")
|
|
1239
|
+
@click.option('--force', is_flag=True, help='Skip confirmation prompt')
|
|
1240
|
+
def ai_logout(force):
|
|
1241
|
+
"""Alias for 'das ai clear'"""
|
|
1242
|
+
_ai_clear_impl(force)
|
|
1243
|
+
|
|
1244
|
+
if __name__ == "__main__":
|
|
1245
|
+
cli()
|