das-cli 1.3.15__tar.gz → 1.4.0__tar.gz

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.
Files changed (50) hide show
  1. {das_cli-1.3.15/das_cli.egg-info → das_cli-1.4.0}/PKG-INFO +9 -1
  2. {das_cli-1.3.15 → das_cli-1.4.0}/README.md +8 -0
  3. {das_cli-1.3.15 → das_cli-1.4.0}/das/app.py +3 -1
  4. {das_cli-1.3.15 → das_cli-1.4.0}/das/cli.py +63 -0
  5. {das_cli-1.3.15 → das_cli-1.4.0}/das/common/entry_fields_constants.py +1 -0
  6. {das_cli-1.3.15 → das_cli-1.4.0}/das/managers/entries_manager.py +75 -6
  7. das_cli-1.4.0/das/services/das_elastic_search.py +26 -0
  8. {das_cli-1.3.15 → das_cli-1.4.0/das_cli.egg-info}/PKG-INFO +9 -1
  9. {das_cli-1.3.15 → das_cli-1.4.0}/das_cli.egg-info/SOURCES.txt +1 -0
  10. {das_cli-1.3.15 → das_cli-1.4.0}/pyproject.toml +1 -1
  11. {das_cli-1.3.15 → das_cli-1.4.0}/tests/entries_manager_test.py +21 -0
  12. {das_cli-1.3.15 → das_cli-1.4.0}/LICENSE +0 -0
  13. {das_cli-1.3.15 → das_cli-1.4.0}/MANIFEST.in +0 -0
  14. {das_cli-1.3.15 → das_cli-1.4.0}/das/__init__.py +0 -0
  15. {das_cli-1.3.15 → das_cli-1.4.0}/das/ai/plugins/dasai.py +0 -0
  16. {das_cli-1.3.15 → das_cli-1.4.0}/das/ai/plugins/entries/entries_plugin.py +0 -0
  17. {das_cli-1.3.15 → das_cli-1.4.0}/das/authentication/auth.py +0 -0
  18. {das_cli-1.3.15 → das_cli-1.4.0}/das/authentication/oauth.py +0 -0
  19. {das_cli-1.3.15 → das_cli-1.4.0}/das/authentication/secure_input.py +0 -0
  20. {das_cli-1.3.15 → das_cli-1.4.0}/das/common/api.py +0 -0
  21. {das_cli-1.3.15 → das_cli-1.4.0}/das/common/config.py +0 -0
  22. {das_cli-1.3.15 → das_cli-1.4.0}/das/common/enums.py +0 -0
  23. {das_cli-1.3.15 → das_cli-1.4.0}/das/common/file_utils.py +0 -0
  24. {das_cli-1.3.15 → das_cli-1.4.0}/das/managers/__init__.py +0 -0
  25. {das_cli-1.3.15 → das_cli-1.4.0}/das/managers/digital_objects_manager.py +0 -0
  26. {das_cli-1.3.15 → das_cli-1.4.0}/das/managers/download_manager.py +0 -0
  27. {das_cli-1.3.15 → das_cli-1.4.0}/das/managers/search_manager.py +0 -0
  28. {das_cli-1.3.15 → das_cli-1.4.0}/das/services/attributes.py +0 -0
  29. {das_cli-1.3.15 → das_cli-1.4.0}/das/services/cache.py +0 -0
  30. {das_cli-1.3.15 → das_cli-1.4.0}/das/services/digital_objects.py +0 -0
  31. {das_cli-1.3.15 → das_cli-1.4.0}/das/services/downloads.py +0 -0
  32. {das_cli-1.3.15 → das_cli-1.4.0}/das/services/entries.py +0 -0
  33. {das_cli-1.3.15 → das_cli-1.4.0}/das/services/entry_fields.py +0 -0
  34. {das_cli-1.3.15 → das_cli-1.4.0}/das/services/hangfire.py +0 -0
  35. {das_cli-1.3.15 → das_cli-1.4.0}/das/services/search.py +0 -0
  36. {das_cli-1.3.15 → das_cli-1.4.0}/das/services/service_base.py +0 -0
  37. {das_cli-1.3.15 → das_cli-1.4.0}/das/services/users.py +0 -0
  38. {das_cli-1.3.15 → das_cli-1.4.0}/das_cli.egg-info/dependency_links.txt +0 -0
  39. {das_cli-1.3.15 → das_cli-1.4.0}/das_cli.egg-info/entry_points.txt +0 -0
  40. {das_cli-1.3.15 → das_cli-1.4.0}/das_cli.egg-info/requires.txt +0 -0
  41. {das_cli-1.3.15 → das_cli-1.4.0}/das_cli.egg-info/top_level.txt +0 -0
  42. {das_cli-1.3.15 → das_cli-1.4.0}/setup.cfg +0 -0
  43. {das_cli-1.3.15 → das_cli-1.4.0}/tests/__init__.py +0 -0
  44. {das_cli-1.3.15 → das_cli-1.4.0}/tests/attributes_test.py +0 -0
  45. {das_cli-1.3.15 → das_cli-1.4.0}/tests/download_manager_test.py +0 -0
  46. {das_cli-1.3.15 → das_cli-1.4.0}/tests/entries_service_test.py +0 -0
  47. {das_cli-1.3.15 → das_cli-1.4.0}/tests/entries_test.py +0 -0
  48. {das_cli-1.3.15 → das_cli-1.4.0}/tests/file_utils_test.py +0 -0
  49. {das_cli-1.3.15 → das_cli-1.4.0}/tests/run_tests.py +0 -0
  50. {das_cli-1.3.15 → das_cli-1.4.0}/tests/search_manager_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: das-cli
3
- Version: 1.3.15
3
+ Version: 1.4.0
4
4
  Summary: DAS api client.
5
5
  Author: Royal Netherlands Institute for Sea Research
6
6
  License-Expression: MIT
@@ -1201,3 +1201,11 @@ MIT License
1201
1201
  ## Maintainers
1202
1202
 
1203
1203
  This project is maintained by the Royal Netherlands Institute for Sea Research (NIOZ).
1204
+
1205
+ ## Debugging and Troubleshooting
1206
+
1207
+ If you need to run the current module use:
1208
+
1209
+ ```bash
1210
+ python -m das.cli login --api-url https://api.das-dev.nioz.nl/ --oauth
1211
+ ```
@@ -1178,3 +1178,11 @@ MIT License
1178
1178
  ## Maintainers
1179
1179
 
1180
1180
  This project is maintained by the Royal Netherlands Institute for Sea Research (NIOZ).
1181
+
1182
+ ## Debugging and Troubleshooting
1183
+
1184
+ If you need to run the current module use:
1185
+
1186
+ ```bash
1187
+ python -m das.cli login --api-url https://api.das-dev.nioz.nl/ --oauth
1188
+ ```
@@ -5,6 +5,7 @@ from das.services.entries import EntriesService
5
5
  from das.services.entry_fields import EntryFieldsService
6
6
  from das.services.hangfire import HangfireService
7
7
  from das.services.search import SearchService
8
+ from das.services.das_elastic_search import DasElasticSearch
8
9
  from das.common.config import save_api_url
9
10
 
10
11
  # $env:PYTHONPATH="C:\Workspace\das-cli"
@@ -20,7 +21,8 @@ class Das:
20
21
  self.entries = EntriesService(base_url)
21
22
  self.hangfire = HangfireService(base_url)
22
23
  self.entry_fields = EntryFieldsService(base_url)
23
- self.search = SearchService(base_url)
24
+ self.search = SearchService(base_url)
25
+ self.elastic_search = DasElasticSearch(base_url)
24
26
 
25
27
  def authenticate(self, username: str, password: str) -> str:
26
28
  """
@@ -395,6 +395,69 @@ def sync_doi(das_ctx, id):
395
395
  except Exception as e:
396
396
  click.secho(f"Error: {e}", fg="red")
397
397
 
398
+ # Elastic Search commands group
399
+ @cli.group()
400
+ def elastic():
401
+ """Commands for working with Elastic Search"""
402
+ pass
403
+
404
+ @elastic.command("search-item")
405
+ @click.option('--field', required=True, help='The field to search by (e.g., identifier)')
406
+ @click.option('--value', required=True, help='The value to search for')
407
+ @click.option('--format', 'output_format', type=click.Choice(['table', 'json', 'compact']), default='table', help='Output format (default: table)')
408
+ @pass_das_context
409
+ def elastic_search_item(das_ctx, field, value, output_format):
410
+ """Search for an item in Elastic Search by field and value"""
411
+ client = das_ctx.get_client()
412
+ try:
413
+ result = client.elastic_search.search_item(field, value)
414
+
415
+ if result is None:
416
+ click.secho(f"No results found for {field}={value}", fg="yellow")
417
+ return
418
+
419
+ if output_format == 'json':
420
+ click.echo(json.dumps(result, indent=2))
421
+ elif output_format == 'compact':
422
+ if isinstance(result, list):
423
+ for item in result:
424
+ if isinstance(item, dict):
425
+ click.echo(', '.join(f"{k}={v}" for k, v in item.items()))
426
+ else:
427
+ click.echo(str(item))
428
+ elif isinstance(result, dict):
429
+ click.echo(', '.join(f"{k}={v}" for k, v in result.items()))
430
+ else:
431
+ click.echo(str(result))
432
+ else:
433
+ # table format (default)
434
+ click.secho(f"\n Search results for {field}={value}", fg="cyan", bold=True)
435
+ click.echo()
436
+
437
+ if isinstance(result, dict):
438
+ max_key_len = max(len(str(k)) for k in result.keys()) if result else 0
439
+ for key, val in result.items():
440
+ if isinstance(val, (dict, list)):
441
+ val_str = json.dumps(val, indent=2)
442
+ click.echo(f" {click.style(str(key).ljust(max_key_len), fg='green')} {val_str}")
443
+ else:
444
+ click.echo(f" {click.style(str(key).ljust(max_key_len), fg='green')} {val}")
445
+ elif isinstance(result, list):
446
+ if result and isinstance(result[0], dict):
447
+ headers = list(result[0].keys())
448
+ rows = [[str(item.get(h, '')) for h in headers] for item in result]
449
+ click.echo(format_table(rows, headers))
450
+ else:
451
+ for item in result:
452
+ click.echo(f" {item}")
453
+ else:
454
+ click.echo(f" {result}")
455
+
456
+ click.echo()
457
+ click.secho(f"✓ Search completed successfully", fg="green")
458
+ except Exception as e:
459
+ click.secho(f"Error: {e}", fg="red")
460
+
398
461
  # Entries commands group
399
462
  @cli.group()
400
463
  def entry():
@@ -2,3 +2,4 @@
2
2
  SELECT_COMBO_INPUT = 4
3
3
  DIGITAL_OBJECT_INPUT = 13
4
4
  GROUP_BOX_INPUT = 34
5
+ SUB_FORM_RELATIONS = 33
@@ -2,9 +2,10 @@ import json
2
2
  from das.common.config import load_api_url
3
3
  from das.managers.search_manager import SearchManager
4
4
  from das.services.attributes import AttributesService
5
+ from das.services.das_elastic_search import DasElasticSearch
5
6
  from das.services.entry_fields import EntryFieldsService
6
7
  from das.services.entries import EntriesService
7
- from das.common.entry_fields_constants import DIGITAL_OBJECT_INPUT, SELECT_COMBO_INPUT, GROUP_BOX_INPUT
8
+ from das.common.entry_fields_constants import DIGITAL_OBJECT_INPUT, SELECT_COMBO_INPUT, GROUP_BOX_INPUT, SUB_FORM_RELATIONS
8
9
  from das.services.search import SearchService
9
10
  from das.services.users import UsersService
10
11
 
@@ -22,6 +23,7 @@ class EntryManager:
22
23
  self.search_service = SearchService(base_url)
23
24
  self.attribute_service = AttributesService(base_url)
24
25
  self.user_service = UsersService(base_url)
26
+ self.das_elastic_search = DasElasticSearch(base_url)
25
27
 
26
28
  def delete(self, id: str = None, code: str = None) -> bool:
27
29
  """Delete an entry by its id or code."""
@@ -324,8 +326,9 @@ class EntryManager:
324
326
  elif column_name in entry:
325
327
  updated_entry[column_name] = self.__get_value(field, entry[column_name])
326
328
 
327
- return self.entry_service.update(attribute_id=attribute_id, entry=updated_entry)
328
-
329
+ return self.entry_service.update(attribute_id=attribute_id, entry=updated_entry)
330
+
331
+
329
332
  def __set_group_box_field_value(self, current_entry: dict, field, entry: dict) -> dict:
330
333
  """Helper method to set group box field value."""
331
334
  custom_data = field.get('customData', None)
@@ -347,12 +350,78 @@ class EntryManager:
347
350
  if field.get('inputType') == SELECT_COMBO_INPUT: # SELECT_COMBO_INPUT
348
351
  select_value = self.__get_select_combobox_field_value(field, source)
349
352
  return select_value
350
- elif field.get('inputType') == GROUP_BOX_INPUT:
351
- # group_box_value = self.__get_group_box_field_value(entry_raw, field)
353
+ elif field.get('inputType') == GROUP_BOX_INPUT:
352
354
  return None
355
+ elif field.get('inputType') == SUB_FORM_RELATIONS:
356
+ sub_form_relations_value = self.__get_sub_form_relations_field_value(field, source)
357
+ return sub_form_relations_value
353
358
  else:
354
- return source
359
+ return source
355
360
 
361
+ def __get_sub_form_relations_field_value(self, field, source: str | list | dict) -> str | list | dict:
362
+ if not source:
363
+ return None
364
+
365
+ if isinstance(source, (dict, list)):
366
+ return source
367
+
368
+ # convert all keys to lowercase
369
+ field = {str(k).lower(): v for k, v in field.items()}
370
+
371
+ tag = None
372
+ values = [v.strip() for v in source.split(',') if v.strip()]
373
+ relations = []
374
+
375
+ if field.get('customdata', None) is not None:
376
+ try:
377
+ customdata_string = field.get('customdata').lower()
378
+ customdata = json.loads(customdata_string)
379
+ if customdata is not None and isinstance(customdata, dict):
380
+ if "tag" in customdata:
381
+ tag = customdata.get("tag")
382
+ if "relations" in customdata:
383
+ relations = customdata.get("relations")
384
+ except json.JSONDecodeError:
385
+ raise ValueError(f"Invalid customdata JSON: {field.get('customdata')}")
386
+
387
+ result_list = []
388
+ for value in values:
389
+ response = self.das_elastic_search.search_item(field="identifier", value=value)
390
+ if response.get('item') is not None and isinstance(response.get('item'), dict):
391
+ if response.get('item').get('source') is not None and isinstance(response.get('item').get('source'), dict):
392
+ source = response.get('item').get('source', {})
393
+ # find the attribute id in the relation.attributeid using lambda and check if its the same as the source.attributeid, if not raise an error
394
+ if relations and not any(rel.get('attributeid') == source.get('attributeid') for rel in relations):
395
+ raise ValueError(f"Attribute ID {source.get('attributeid')} for value '{value}' is not allowed in the relations list")
396
+ item = {
397
+ "id": source.get('id'),
398
+ "name": source.get('title'),
399
+ "code": source.get('code'),
400
+ "attributeId": source.get('attributeid'),
401
+ "tag": tag,
402
+ "acronym": None,
403
+ "order": 0
404
+ }
405
+ # get the attribute name from the relations where the attribute id is the same as the source.attributeid using lambda
406
+ attribute_name = next((attr.get('attributealias') for attr in relations if attr.get('attributeid') == source.get('attributeid')), None)
407
+
408
+ # We need to check if the relation item already exists in the result list, if it does we need to append the item to the relations list of that item, if not we need to create a new item in the result list
409
+ existing_item = next((i for i in result_list if i.get('attribute') == attribute_name), None)
410
+ if existing_item:
411
+ existing_item.get('relations', []).append(item)
412
+ continue
413
+
414
+ item_relation = {
415
+ "attribute": attribute_name,
416
+ "relations": [item],
417
+ "tag": tag
418
+ }
419
+
420
+ result_list.append(item_relation)
421
+
422
+ return json.dumps(result_list) if result_list else source
423
+
424
+
356
425
  def __get_select_combobox_field_value(self, field, source: str | list | dict) -> str | list | dict:
357
426
  """Helper method to get select combobox field value."""
358
427
 
@@ -0,0 +1,26 @@
1
+ from das.services.service_base import ServiceBase
2
+ from das.common.api import post_data
3
+
4
+ class DasElasticSearch(ServiceBase):
5
+ def __init__(self, base_url):
6
+ self.base_url = f"{base_url}/api/services/app/DataPortal"
7
+
8
+ def search_item(self, field: str, value: str):
9
+ headers = {**self.set_auth_headers(), "Content-Type": "application/json"}
10
+ url = f"{self.base_url}/SearchItem"
11
+
12
+ payload = {
13
+ "field": field,
14
+ "value": value
15
+ }
16
+
17
+ response = post_data(url, data=payload, headers=headers)
18
+
19
+ if response.get('success') == True:
20
+ return response.get('result')
21
+ else:
22
+ error = response.get('error', 'Unknown error')
23
+ raw_content = response.get('raw_content', '')
24
+ if raw_content:
25
+ raise ValueError(f"{error}\nResponse content: {raw_content}")
26
+ raise ValueError(str(error))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: das-cli
3
- Version: 1.3.15
3
+ Version: 1.4.0
4
4
  Summary: DAS api client.
5
5
  Author: Royal Netherlands Institute for Sea Research
6
6
  License-Expression: MIT
@@ -1201,3 +1201,11 @@ MIT License
1201
1201
  ## Maintainers
1202
1202
 
1203
1203
  This project is maintained by the Royal Netherlands Institute for Sea Research (NIOZ).
1204
+
1205
+ ## Debugging and Troubleshooting
1206
+
1207
+ If you need to run the current module use:
1208
+
1209
+ ```bash
1210
+ python -m das.cli login --api-url https://api.das-dev.nioz.nl/ --oauth
1211
+ ```
@@ -23,6 +23,7 @@ das/managers/entries_manager.py
23
23
  das/managers/search_manager.py
24
24
  das/services/attributes.py
25
25
  das/services/cache.py
26
+ das/services/das_elastic_search.py
26
27
  das/services/digital_objects.py
27
28
  das/services/downloads.py
28
29
  das/services/entries.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "das-cli"
7
- version = "1.3.15"
7
+ version = "1.4.00"
8
8
  authors = [
9
9
  { name="Royal Netherlands Institute for Sea Research" },
10
10
  ]
@@ -78,6 +78,27 @@ class TestEntriesManager(unittest.TestCase):
78
78
  self.assertIsNotNone(created_event)
79
79
  self.assertRegex(created_event[0]['id'], r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
80
80
 
81
+ def test_create_analytical_run_entry(self):
82
+ """Test creating an analytical run entry"""
83
+
84
+ payload = {
85
+ 'Alias': 'alias-test',
86
+ 'Instrument': 'k.b.db',
87
+ 'Date': '2024-06-01',
88
+ 'Protocol(s)': 'zc.b.v,zc.b.t',
89
+ 'Comment': 'This is a test comment',
90
+ 'Operator': 'Wim Boer',
91
+ 'Software': 'q.b.b',
92
+ 'Sample(s)': "4.b.x3g,4.b.p3g,4.b.w3g,zb.b.bc,zb.b.lk,zb.b.k1",
93
+ 'Standard(s)': "wc.b.4,wc.b.k"
94
+ }
95
+
96
+ created_entry_id = self.entry_manager.create(attribute="Analytical Run", entry=payload)
97
+
98
+ self.assertIsNotNone(created_entry_id)
99
+ # assert createrd entry id is an guid
100
+ self.assertRegex(created_entry_id[0]['id'], r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
101
+
81
102
  def test_delete_entry(self):
82
103
  """Test deleting an entry"""
83
104
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes