das-cli 1.0.14__py3-none-any.whl → 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of das-cli might be problematic. Click here for more details.

das/cli.py CHANGED
@@ -551,6 +551,41 @@ def create_entry(das_ctx, attribute, file_path=None, data=None):
551
551
  except Exception as e:
552
552
  click.secho(f"Error: {e}", fg="red")
553
553
 
554
+ @entry.command("upload-digital-object")
555
+ @click.option('--entry-code', required=True, help='Entry code to attach the digital object to')
556
+ @click.option('--type', 'digital_object_type', required=True, help='Digital object type name (e.g., Dataset, File, Image)')
557
+ @click.option('--description', 'file_description', default='', help='Description for the uploaded file')
558
+ @click.argument('file_path', required=True)
559
+ @pass_das_context
560
+ def upload_digital_object(das_ctx, entry_code, digital_object_type, file_description, file_path):
561
+ """Upload a file as a digital object and link it to an entry.
562
+
563
+ Examples:
564
+
565
+ \b
566
+ # Upload a dataset file and link to an entry
567
+ das entry upload-digital-object --entry-code ENT001 --type Dataset --description "CTD raw" c:\\data\\ctd.zip
568
+ """
569
+ try:
570
+ # Ensure services are initialized
571
+ das_ctx.get_client()
572
+
573
+ # Perform upload and link
574
+ digital_object_id = das_ctx.digital_objects_manager.upload_digital_object(
575
+ entry_code=entry_code,
576
+ file_description=file_description,
577
+ digital_object_type=digital_object_type,
578
+ file_path=file_path,
579
+ )
580
+
581
+ if digital_object_id:
582
+ click.secho("✓ Digital object uploaded and linked successfully!", fg="green")
583
+ click.echo(f"Digital Object ID: {digital_object_id}")
584
+ else:
585
+ click.secho("Upload completed but no ID was returned.", fg="yellow")
586
+ except Exception as e:
587
+ click.secho(f"Error: {e}", fg="red")
588
+
554
589
  @entry.command("get")
555
590
  @click.option('--code', default=None, help='Entry code')
556
591
  @click.option('--id', type=int, default=None, help='Entry ID')
@@ -1,16 +1,24 @@
1
+ import os
2
+ import sys
1
3
  from das.common.config import load_api_url
4
+ from das.services.search import SearchService
2
5
  from das.services.entries import EntriesService
3
6
  from das.services.digital_objects import DigitalObjectsService
4
7
 
8
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
5
9
 
6
10
  class DigitalObjectsManager:
11
+ """Manager for digital objects."""
12
+
7
13
  def __init__(self):
8
14
  base_url = load_api_url()
9
15
  if base_url is None or base_url == "":
10
16
  raise ValueError(f"Base URL is required - {self.__class__.__name__}")
11
17
 
18
+ self.__attribute_id_digital_object_type = 5;
12
19
  self.digital_objects_service = DigitalObjectsService(base_url)
13
20
  self.entry_service = EntriesService(base_url)
21
+ self.search_service = SearchService(base_url)
14
22
 
15
23
  def link_existing_digital_objects(
16
24
  self, entry_code: str, digital_object_code_list: list[str], is_unlink: bool = False
@@ -43,4 +51,34 @@ class DigitalObjectsManager:
43
51
 
44
52
  return result
45
53
 
54
+ def upload_digital_object(self, entry_code: str, file_description: str, digital_object_type: str, file_path: str):
55
+ """Upload a digital object to the digital object service."""
56
+ response = self.search_service.search_entries(
57
+ queryString=f"displayname({digital_object_type})",
58
+ attributeId=self.__attribute_id_digital_object_type,
59
+ maxResultCount=1,
60
+ skipCount=0
61
+ )
46
62
 
63
+ entry_response = self.entry_service.get_entry(entry_code)
64
+ if entry_response is None:
65
+ raise ValueError(f"Entry with code '{entry_code}' not found")
66
+
67
+ if response.get('totalCount', 0) == 0:
68
+ raise ValueError(f"Digital object type '{digital_object_type}' not found")
69
+
70
+ digital_object_type_id = response.get('items', [])[0].get('entry').get('id')
71
+ digital_object_id = self.digital_objects_service.upload_digital_object(file_description, digital_object_type_id, file_path)
72
+
73
+ self.digital_objects_service.link_existing_digital_objects(
74
+ attribute_id=entry_response.get('attributeId'),
75
+ entry_id=entry_response.get('entry').get('id'),
76
+ digital_object_id_list=[digital_object_id]
77
+ )
78
+
79
+ return digital_object_id
80
+
81
+
82
+ if __name__ == "__main__":
83
+ digital_objects_manager = DigitalObjectsManager()
84
+ digital_objects_manager.upload_digital_object(entry_code="zb.b.f7", file_description="test", digital_object_type="Dataset", file_path="my_new_file.txt")
@@ -3,7 +3,6 @@ from das.services.attributes import AttributesService
3
3
  from das.services.entry_fields import EntryFieldsService
4
4
  from das.services.search import SearchService
5
5
 
6
-
7
6
  class SearchManager:
8
7
  def __init__(self):
9
8
  base_url = load_api_url()
@@ -1,10 +1,22 @@
1
+ import os
2
+ import sys
3
+ from math import ceil
4
+ from os.path import exists
5
+ import json
6
+ from base64 import b64encode
1
7
  from das.common.api import post_data
2
- from das.common.config import load_token
3
-
8
+ from das.common.config import load_token, load_verify_ssl
9
+ from pathlib import Path
10
+ import math
11
+ import uuid
12
+ import requests
4
13
 
14
+ CHUNK_SIZE = 1000000 # 1MB
5
15
  class DigitalObjectsService:
6
16
  def __init__(self, base_url):
7
17
  self.base_url = f"{base_url}/api/services/app/DigitalObject"
18
+ # Common possible upload endpoints observed across deployments
19
+ self.upload_digital_object_url = f"{base_url}/File/UploadDigitalObject"
8
20
 
9
21
  def link_existing_digital_objects(self, attribute_id: int, entry_id: str, digital_object_id_list: list[str], is_unlink: bool = False):
10
22
  """Link existing digital objects to an entry."""
@@ -39,6 +51,92 @@ class DigitalObjectsService:
39
51
  )
40
52
 
41
53
  return response.get("success")
54
+
55
+ # This is our chunk reader. This is what gets the next chunk of data ready to send.
56
+ def __read_in_chunks(self, file_object, chunk_size):
57
+ while True:
58
+ data = file_object.read(chunk_size)
59
+ if not data:
60
+ break
61
+ yield data
62
+
63
+
64
+ def upload_digital_object(self, file_description: str, digital_object_type_id: str, file_path: str):
65
+
66
+ if not exists(file_path):
67
+ raise ValueError(f"File '{file_path}' does not exist")
68
+
69
+ head, tail = os.path.split(file_path)
70
+
71
+ metadata = {
72
+ "fileName": tail,
73
+ "fileSize": os.path.getsize(file_path),
74
+ "description": file_description,
75
+ "digitalObjectTypeId": digital_object_type_id,
76
+ "id": str(uuid.uuid4()).lower(),
77
+ "description": file_description,
78
+ "totalCount": ceil(os.path.getsize(file_path) / CHUNK_SIZE),
79
+ "index": 0,
80
+ }
81
+
82
+ binary_file = open(file_path, "rb")
83
+ index = 0
84
+ offset = 0
85
+ digital_object_id = None
86
+ headers = {}
87
+
88
+ try:
89
+ for chunk in self.__read_in_chunks(binary_file, CHUNK_SIZE):
90
+ offset = index + len(chunk)
91
+ headers['Content-Range'] = 'bytes %s-%s/%s' % (index, offset - 1, metadata.get('fileSize'))
92
+ index = offset
93
+ json_metadata = json.dumps(metadata)
94
+ base654_bytes = b64encode(json_metadata.encode('utf-8')).decode('ascii')
95
+ headers['metadata'] = base654_bytes
96
+
97
+ r = self.upload_file(chunk, metadata, headers)
98
+
99
+ if r.get('result', None) is None:
100
+ continue
101
+
102
+ digital_object_id = r.get('result').get('id')
103
+ metadata['index'] = index + 1
104
+
105
+ binary_file.close()
106
+
107
+ except Exception as e:
108
+ raise ValueError(f"Error uploading file '{file_path}': {str(e)}")
109
+ finally:
110
+ binary_file.close()
111
+
112
+ return digital_object_id
113
+
114
+
115
+
116
+ def upload_file(self, file, body, headers):
117
+ """Upload a file to the digital object service."""
118
+ token = load_token()
119
+ headers.update({
120
+ "Accept": "application/json",
121
+ "Authorization": f"Bearer {token}",
122
+ # Do NOT set Content-Type here when sending files; requests will set proper multipart boundary
123
+ })
124
+
125
+ files = {
126
+ "file": ("chunk", file, "application/octet-stream"),
127
+ }
128
+
129
+ try:
130
+ response = requests.post(self.upload_digital_object_url, headers=headers, files=files, verify=load_verify_ssl())
131
+ response.raise_for_status()
132
+ if response.status_code == 200:
133
+ return response.json()
134
+ else:
135
+ raise ValueError(f"Error uploading file: {response.status_code} - {response.text}")
136
+ except requests.RequestException as e:
137
+ raise ValueError(f"Error uploading file: {str(e)}")
138
+
139
+
42
140
 
43
141
 
44
142
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: das-cli
3
- Version: 1.0.14
3
+ Version: 1.1.0
4
4
  Summary: DAS api client.
5
5
  Author: Royal Netherlands Institute for Sea Research
6
6
  License-Expression: MIT
@@ -166,6 +166,30 @@ das entry update --attribute <AttributeName> [--code CODE] <file_path>
166
166
  # das entry update --attribute core --data [{ 'Code': 'ENT001' }, { 'Code': 'ENT002' }]
167
167
  ```
168
168
 
169
+ #### Upload and link a digital object
170
+
171
+ ```bash
172
+ # Upload a file as a digital object and link it to an entry
173
+ das entry upload-digital-object --entry-code ENT001 --type Dataset --description "CTD raw" c:\data\ctd.zip
174
+ ```
175
+
176
+ #### Link or unlink digital objects
177
+
178
+ ```bash
179
+ # Link digital objects by their codes to an entry
180
+ das entry link-digital-objects --entry-code ENT001 -d DO001 -d DO002
181
+
182
+ # Unlink digital objects from an entry
183
+ das entry link-digital-objects --entry-code ENT001 -d DO003 --unlink
184
+ ```
185
+
186
+ #### Change ownership
187
+
188
+ ```bash
189
+ # Transfer ownership of one or more entries to a user
190
+ das entry chown --user alice --code ENT001 --code ENT002
191
+ ```
192
+
169
193
  ### Hangfire
170
194
 
171
195
  ```bash
@@ -216,6 +240,19 @@ das config ssl-status
216
240
  das config reset --force
217
241
  ```
218
242
 
243
+ ### AI
244
+
245
+ ```bash
246
+ # Start interactive DAS AI session (prompts for OpenAI API key if needed)
247
+ das ai enable
248
+
249
+ # Clear saved OpenAI key and auth token
250
+ das ai clear [--force]
251
+
252
+ # Alias for `das ai clear`
253
+ das ai logout [--force]
254
+ ```
255
+
219
256
  ## Python API
220
257
 
221
258
  ### Basic Usage
@@ -1,6 +1,6 @@
1
1
  das/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  das/app.py,sha256=kKxN4Vn84SA5Ph3zY13avMG2vrUp-ffpdDkhwYR9Bho,1475
3
- das/cli.py,sha256=3BujNatTZXqXGoDNleG8ru4GT_KkUDRQujesAyGRP0w,46971
3
+ das/cli.py,sha256=vt0EiRuoqVXrMP8n1_k_Tx-4eMGM7uAcCf3xWMi7nsA,48523
4
4
  das/ai/plugins/dasai.py,sha256=R0X0Vey_GOAtWoqcloB-NATZFtXB_l5b9dfPXocNIbI,2165
5
5
  das/ai/plugins/entries/entries_plugin.py,sha256=Dhv6PrguQj5mzxBW6DlCzkmwucszazLQfzwlp9EhIGk,608
6
6
  das/authentication/auth.py,sha256=DTtH66Ft6nuuMe7EYvrr3GqGVEGGxE7GmD2fO7vRv4s,1501
@@ -11,22 +11,22 @@ das/common/entry_fields_constants.py,sha256=5Yh4Ujt70HEF-FsnwVBPBm3DB3HHzQWSWR-9
11
11
  das/common/enums.py,sha256=jS0frv6717duG_wZNockXMTZ-VfsGu_f8_-lgYGnrcY,1745
12
12
  das/common/file_utils.py,sha256=-zePjYsj8iRpQssVQMHDK3Mh5q8FooKJCUCKCXKS6_Y,7006
13
13
  das/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- das/managers/digital_objects_manager.py,sha256=i6wDx589dEQ58X_pZGON808GkWinymV3PZAZ4TVx1P0,1825
14
+ das/managers/digital_objects_manager.py,sha256=v7VAYfKoDpmWJGVgpVoSyk6hqGMiQJeOX5rgm65xE5U,3677
15
15
  das/managers/download_manager.py,sha256=NqaLhmjw-4nZq8SVdN0g5MAnbMPEnu-A3A4oXxQ-IZQ,3776
16
16
  das/managers/entries_manager.py,sha256=Kc_PN71Bp7VICrxoP9MiDCrUzR7OdUXfX5puDDxtm08,19015
17
- das/managers/search_manager.py,sha256=kad6-3H-LsYlUVPE7yKEaCuKLpN2w2-eE3Baskg-R-0,4265
17
+ das/managers/search_manager.py,sha256=vXf0JmK5oW-xEGUdDnppfc1-6HdH1hfiZR7L2bCz9u0,4263
18
18
  das/services/attributes.py,sha256=78E9f1wNZYxG9Hg5HfX_h1CFmACaMjwD2Y6Ilb7PJGY,2616
19
19
  das/services/cache.py,sha256=g-vY51gqGV_1Vpza476PkMqGpuDNo1NbTwQWIIsvO0s,1932
20
- das/services/digital_objects.py,sha256=_1X1PqQ2X0_0--tO4YRuNXQEiZIyKSPEYSwROliTc8o,1394
20
+ das/services/digital_objects.py,sha256=ww1KHVLNmm_ffzgqP4Jt4wCbHMVfhD2FJWahlSPFaes,4935
21
21
  das/services/downloads.py,sha256=YBDFPmjAQHUK0OUdFprW1Ox81nzpKaJE9xQBJyyEz4Q,3060
22
22
  das/services/entries.py,sha256=Uspl7LZcNWEnr7ct5_Kn31jMjrkSKV7UXzrN6nb3HF0,4966
23
23
  das/services/entry_fields.py,sha256=x2wUDkKNduj9pf4s56hRo0UW-eBhipkU9gFMEjFw5DA,1290
24
24
  das/services/hangfire.py,sha256=hidmVP9yb4znzBaJJRyKawYx7oYaBv5OVL-t0BhvN_A,818
25
25
  das/services/search.py,sha256=3X_KPb9fs024FhxoTr4j-xY5ymm5rvvzlekxuh8tLdg,1374
26
26
  das/services/users.py,sha256=iNijO2UPIEtcpPy8Tkemdxxym9rYLCUyckQHIQj68W0,795
27
- das_cli-1.0.14.dist-info/licenses/LICENSE,sha256=4EDhysVgQWBlzo0rdUl_k89s-iVfgCcSa1gUx1TM1vA,1124
28
- das_cli-1.0.14.dist-info/METADATA,sha256=qgGipoACenC83kC78nlcbBd8QtCd_O0ETDEFJlY1qBs,10470
29
- das_cli-1.0.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
- das_cli-1.0.14.dist-info/entry_points.txt,sha256=ZrdMae7NcvogQhzM1zun8E8n_QwYq-LpZvoJCr2_I4g,36
31
- das_cli-1.0.14.dist-info/top_level.txt,sha256=OJsPEeJyJ2rJlpEn2DTPgbMSvYG-6FeD13_m5qLpw3E,4
32
- das_cli-1.0.14.dist-info/RECORD,,
27
+ das_cli-1.1.0.dist-info/licenses/LICENSE,sha256=4EDhysVgQWBlzo0rdUl_k89s-iVfgCcSa1gUx1TM1vA,1124
28
+ das_cli-1.1.0.dist-info/METADATA,sha256=yj8bEIJ_zMR-QxYGgebiV8s6tuAyVBkJGUL-2ij_oEM,11375
29
+ das_cli-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
+ das_cli-1.1.0.dist-info/entry_points.txt,sha256=ZrdMae7NcvogQhzM1zun8E8n_QwYq-LpZvoJCr2_I4g,36
31
+ das_cli-1.1.0.dist-info/top_level.txt,sha256=OJsPEeJyJ2rJlpEn2DTPgbMSvYG-6FeD13_m5qLpw3E,4
32
+ das_cli-1.1.0.dist-info/RECORD,,