cmem-cmemc 25.6.0__py3-none-any.whl → 26.1.0rc1__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.
Files changed (39) hide show
  1. cmem_cmemc/cli.py +11 -6
  2. cmem_cmemc/command.py +1 -1
  3. cmem_cmemc/command_group.py +27 -0
  4. cmem_cmemc/commands/acl.py +388 -20
  5. cmem_cmemc/commands/admin.py +10 -10
  6. cmem_cmemc/commands/client.py +12 -5
  7. cmem_cmemc/commands/config.py +106 -12
  8. cmem_cmemc/commands/dataset.py +162 -118
  9. cmem_cmemc/commands/file.py +117 -73
  10. cmem_cmemc/commands/graph.py +200 -72
  11. cmem_cmemc/commands/graph_imports.py +12 -5
  12. cmem_cmemc/commands/graph_insights.py +61 -25
  13. cmem_cmemc/commands/metrics.py +15 -9
  14. cmem_cmemc/commands/migration.py +12 -4
  15. cmem_cmemc/commands/package.py +548 -0
  16. cmem_cmemc/commands/project.py +155 -22
  17. cmem_cmemc/commands/python.py +8 -4
  18. cmem_cmemc/commands/query.py +119 -25
  19. cmem_cmemc/commands/scheduler.py +6 -4
  20. cmem_cmemc/commands/store.py +2 -1
  21. cmem_cmemc/commands/user.py +124 -24
  22. cmem_cmemc/commands/validation.py +15 -10
  23. cmem_cmemc/commands/variable.py +264 -61
  24. cmem_cmemc/commands/vocabulary.py +18 -13
  25. cmem_cmemc/commands/workflow.py +21 -11
  26. cmem_cmemc/completion.py +105 -105
  27. cmem_cmemc/context.py +38 -8
  28. cmem_cmemc/exceptions.py +8 -2
  29. cmem_cmemc/manual_helper/multi_page.py +0 -1
  30. cmem_cmemc/object_list.py +234 -7
  31. cmem_cmemc/string_processor.py +142 -5
  32. cmem_cmemc/title_helper.py +50 -0
  33. cmem_cmemc/utils.py +8 -7
  34. {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/METADATA +6 -6
  35. cmem_cmemc-26.1.0rc1.dist-info/RECORD +62 -0
  36. {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/WHEEL +1 -1
  37. cmem_cmemc-25.6.0.dist-info/RECORD +0 -61
  38. {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/entry_points.txt +0 -0
  39. {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  """Keycloak client management commands"""
2
2
 
3
3
  import click
4
- from click import ClickException, UsageError
4
+ from click import UsageError
5
5
  from cmem.cmempy.config import get_keycloak_base_uri, get_keycloak_realm_id
6
6
  from cmem.cmempy.keycloak.client import (
7
7
  generate_client_secret,
@@ -13,7 +13,8 @@ from cmem.cmempy.keycloak.client import (
13
13
  from cmem_cmemc import completion
14
14
  from cmem_cmemc.command import CmemcCommand
15
15
  from cmem_cmemc.command_group import CmemcGroup
16
- from cmem_cmemc.context import ApplicationContext
16
+ from cmem_cmemc.context import ApplicationContext, build_caption
17
+ from cmem_cmemc.exceptions import CmemcError
17
18
 
18
19
  NO_CLIENT_ERROR = (
19
20
  "{} is not a valid client account. "
@@ -47,7 +48,13 @@ def list_command(app: ApplicationContext, raw: bool, id_only: bool) -> None:
47
48
  app.echo_info(cnt["clientId"])
48
49
  return
49
50
  table = [(_["clientId"], _.get("description", "-")) for _ in clients]
50
- app.echo_info_table(table, headers=["Client ID", "Description"], sort_column=0)
51
+ app.echo_info_table(
52
+ table,
53
+ headers=["Client ID", "Description"],
54
+ sort_column=0,
55
+ caption=build_caption(len(table), "client"),
56
+ empty_table_message="No client accounts found.",
57
+ )
51
58
 
52
59
 
53
60
  @click.command(cls=CmemcCommand, name="secret")
@@ -66,7 +73,7 @@ def secret_command(app: ApplicationContext, client_id: str, generate: bool, outp
66
73
 
67
74
  clients = get_client_by_client_id(client_id)
68
75
  if not clients:
69
- raise ClickException(NO_CLIENT_ERROR.format(client_id))
76
+ raise CmemcError(NO_CLIENT_ERROR.format(client_id))
70
77
 
71
78
  if generate:
72
79
  if not output:
@@ -106,7 +113,7 @@ def open_command(app: ApplicationContext, client_ids: tuple[str]) -> None:
106
113
  client_id_map = {c["clientId"]: c["id"] for c in clients}
107
114
  for _ in client_ids:
108
115
  if _ not in client_id_map:
109
- raise ClickException(NO_CLIENT_ERROR.format(_))
116
+ raise CmemcError(NO_CLIENT_ERROR.format(_))
110
117
  client_id = client_id_map[_]
111
118
  open_user_uri = f"{open_client_base_uri}/{client_id}/settings"
112
119
 
@@ -1,34 +1,128 @@
1
1
  """configuration commands for cmem command line interface."""
2
2
 
3
3
  import click
4
- from click import ClickException
4
+ from click import Context
5
5
 
6
6
  from cmem_cmemc.command import CmemcCommand
7
7
  from cmem_cmemc.command_group import CmemcGroup
8
- from cmem_cmemc.context import KNOWN_CONFIG_KEYS, ApplicationContext
8
+ from cmem_cmemc.context import KNOWN_CONFIG_KEYS, ApplicationContext, build_caption
9
+ from cmem_cmemc.exceptions import CmemcError
10
+ from cmem_cmemc.object_list import (
11
+ DirectValuePropertyFilter,
12
+ ObjectList,
13
+ compare_regex,
14
+ )
15
+
16
+
17
+ def get_connections(ctx: Context) -> list[dict]:
18
+ """Get connections for object list"""
19
+ app = ctx.obj
20
+ if app is not None:
21
+ config = app.get_config()
22
+ else:
23
+ # when in completion mode, obj is not set :-(
24
+ # return empty list
25
+ return []
26
+
27
+ connections = []
28
+
29
+ for section_name in config.sections():
30
+ if section_name != "DEFAULT":
31
+ section = config[section_name]
32
+ connections.append(
33
+ {
34
+ "name": section_name,
35
+ "base_uri": section.get("CMEM_BASE_URI", "-"),
36
+ "grant_type": section.get("OAUTH_GRANT_TYPE", "-"),
37
+ }
38
+ )
39
+
40
+ return connections
41
+
42
+
43
+ config_list = ObjectList(
44
+ name="connections",
45
+ get_objects=get_connections,
46
+ filters=[
47
+ DirectValuePropertyFilter(
48
+ name="regex",
49
+ description="Filter by regex matching the connection name.",
50
+ property_key="name",
51
+ compare=compare_regex,
52
+ completion_method="none",
53
+ ),
54
+ ],
55
+ )
9
56
 
10
57
 
11
58
  @click.command(cls=CmemcCommand, name="list")
12
- @click.pass_obj
13
- def list_command(app: ApplicationContext) -> None:
59
+ @click.option(
60
+ "--filter",
61
+ "filter_",
62
+ type=(str, str),
63
+ multiple=True,
64
+ help=config_list.get_filter_help_text(),
65
+ shell_complete=config_list.complete_values,
66
+ )
67
+ @click.option(
68
+ "--id-only",
69
+ is_flag=True,
70
+ help="Lists only connection names. "
71
+ "This is useful for piping the names into other cmemc commands.",
72
+ )
73
+ @click.pass_context
74
+ def list_command(ctx: Context, filter_: tuple[tuple[str, str]], id_only: bool) -> None:
14
75
  """List configured connections.
15
76
 
16
- This command lists all configured
17
- connections from the currently used config file.
77
+ This command lists all configured connections from the currently used config file.
78
+ Each connection is listed with its name, base URI, and grant type.
18
79
 
19
80
  The connection identifier can be used with the --connection option
20
81
  in order to use a specific Corporate Memory instance.
21
82
 
83
+ You can use the --filter option to filter connections by regex matching
84
+ the connection name.
85
+
22
86
  In order to apply commands on more than one instance, you need to use
23
87
  typical unix gear such as xargs or parallel.
24
88
 
25
- Example: cmemc config list | xargs -I % sh -c 'cmemc -c % admin status'
89
+ Example: cmemc config list
90
+
91
+ Example: cmemc config list --id-only | xargs -I % sh -c 'cmemc -c % admin status'
26
92
 
27
- Example: cmemc config list | parallel --jobs 5 cmemc -c {} admin status
93
+ Example: cmemc config list --id-only | parallel --jobs 5 cmemc -c {} admin status
28
94
  """
29
- for section_string in sorted(app.get_config(), key=str.casefold):
30
- if section_string != "DEFAULT":
31
- app.echo_result(section_string)
95
+ app = ctx.obj
96
+ connections = config_list.apply_filters(ctx=ctx, filter_=filter_)
97
+
98
+ # Sort connections case-insensitively by name
99
+ connections = sorted(connections, key=lambda c: c["name"].casefold())
100
+
101
+ if id_only:
102
+ for connection in connections:
103
+ app.echo_result(connection["name"])
104
+ return
105
+
106
+ table = [
107
+ (
108
+ connection["name"],
109
+ connection["base_uri"],
110
+ connection["grant_type"],
111
+ )
112
+ for connection in connections
113
+ ]
114
+
115
+ filtered = len(filter_) > 0
116
+ app.echo_info_table(
117
+ table,
118
+ headers=["Connection", "Base URI", "Grant Type"],
119
+ caption=build_caption(
120
+ len(table), "connection", instance=str(app.config_file), filtered=filtered
121
+ ),
122
+ empty_table_message="No connections found for these filters."
123
+ if filtered
124
+ else "No connections found.",
125
+ )
32
126
 
33
127
 
34
128
  @click.command(cls=CmemcCommand, name="edit")
@@ -60,7 +154,7 @@ def get_command(app: ApplicationContext, key: str) -> None:
60
154
  value = KNOWN_CONFIG_KEYS[key]()
61
155
  app.echo_debug(f"Type of {key} value is {type(value)}")
62
156
  if value is None:
63
- raise ClickException(f"Configuration key {key} is not used in this configuration.")
157
+ raise CmemcError(f"Configuration key {key} is not used in this configuration.")
64
158
  app.echo_info(str(value))
65
159
 
66
160
 
@@ -1,11 +1,10 @@
1
1
  """dataset commands for cmem command line interface."""
2
2
 
3
3
  import json
4
- import re
5
4
 
6
5
  import click
7
6
  import requests.exceptions
8
- from click import ClickException, UsageError
7
+ from click import Context, UsageError
9
8
  from cmem.cmempy.config import get_cmem_base_uri
10
9
  from cmem.cmempy.workspace import get_task_plugin_description, get_task_plugins
11
10
  from cmem.cmempy.workspace.projects.datasets.dataset import (
@@ -25,68 +24,68 @@ from cmem_cmemc.command import CmemcCommand
25
24
  from cmem_cmemc.command_group import CmemcGroup
26
25
  from cmem_cmemc.commands.file import _upload_file_resource, resource
27
26
  from cmem_cmemc.completion import get_dataset_file_mapping
28
- from cmem_cmemc.context import ApplicationContext
27
+ from cmem_cmemc.context import ApplicationContext, build_caption
29
28
  from cmem_cmemc.exceptions import CmemcError
29
+ from cmem_cmemc.object_list import (
30
+ DirectListPropertyFilter,
31
+ DirectMultiValuePropertyFilter,
32
+ DirectValuePropertyFilter,
33
+ ObjectList,
34
+ compare_regex,
35
+ transform_extract_labels,
36
+ )
30
37
  from cmem_cmemc.parameter_types.path import ClickSmartPath
31
38
  from cmem_cmemc.smart_path import SmartPath as Path
39
+ from cmem_cmemc.string_processor import DatasetLink, DatasetTypeLink
40
+ from cmem_cmemc.title_helper import DatasetTypeTitleHelper, ProjectTitleHelper
32
41
  from cmem_cmemc.utils import check_or_select_project, struct_to_table
33
42
 
34
- DATASET_FILTER_TYPES = sorted(["project", "regex", "tag", "type"])
35
- DATASET_LIST_FILTER_HELP_TEXT = (
36
- "Filter datasets based on metadata. First parameter"
37
- f" can be one of the following values: {', '.join(DATASET_FILTER_TYPES)}."
38
- " The options for the second parameter depend on the first parameter."
39
- )
40
- DATASET_DELETE_FILTER_HELP_TEXT = (
41
- "Delete datasets based on metadata. First parameter --filter"
42
- f" CHOICE can be one of {DATASET_FILTER_TYPES!s}."
43
- " The second parameter is based on CHOICE."
44
- )
45
-
46
43
 
47
- def _get_dataset_tag_labels(dataset_: dict) -> list[str]:
48
- """Output a list of tag labels from a single dataset."""
49
- return [_["label"] for _ in dataset_["tags"]]
50
-
51
-
52
- def _get_datasets_filtered(
53
- datasets: list[dict], filter_name: str, filter_value: str | int
54
- ) -> list[dict]:
55
- """Get dataset filtered according to filter name and value.
56
-
57
- Args:
58
- ----
59
- datasets: list of datasets to filter
60
- filter_name (str): one of DATASET_FILTER_TYPES
61
- filter_value (str|int): value according to filter
62
-
63
- Returns:
64
- -------
65
- list of filtered datasets from the get_query_status API call
66
-
67
- Raises:
68
- ------
69
- UsageError
70
-
71
- """
72
- if filter_name not in DATASET_FILTER_TYPES:
73
- raise UsageError(
74
- f"{filter_name} is an unknown filter name. " f"Use one of {DATASET_FILTER_TYPES}."
75
- )
76
- # filter by project ID
77
- if filter_name == "project":
78
- return [_ for _ in datasets if _["projectId"] == filter_value]
79
- # filter by regex on the label
80
- if filter_name == "regex":
81
- return [_ for _ in datasets if re.search(str(filter_value), _["label"])]
82
- # filter by dataset type
83
- if filter_name == "type":
84
- return [_ for _ in datasets if re.search(str(filter_value), _["pluginId"])]
85
- # filter by tag label
86
- if filter_name == "tag":
87
- return [_ for _ in datasets if filter_value in _get_dataset_tag_labels(_)]
88
- # default is unfiltered
89
- return datasets
44
+ def get_datasets(ctx: Context) -> list[dict]:
45
+ """Get datasets for object list."""
46
+ _ = ctx
47
+ return list_items(item_type="dataset")["results"] # type: ignore[no-any-return]
48
+
49
+
50
+ dataset_list = ObjectList(
51
+ name="datasets",
52
+ get_objects=get_datasets,
53
+ filters=[
54
+ DirectValuePropertyFilter(
55
+ name="project",
56
+ description="Filter by project ID.",
57
+ property_key="projectId",
58
+ completion_method="values",
59
+ title_helper=ProjectTitleHelper(),
60
+ ),
61
+ DirectValuePropertyFilter(
62
+ name="regex",
63
+ description="Filter by regex matching the dataset label.",
64
+ property_key="label",
65
+ compare=compare_regex,
66
+ completion_method="none",
67
+ ),
68
+ DirectValuePropertyFilter(
69
+ name="type",
70
+ description="Filter by dataset type.",
71
+ property_key="pluginId",
72
+ compare=compare_regex,
73
+ completion_method="values",
74
+ title_helper=DatasetTypeTitleHelper(),
75
+ ),
76
+ DirectListPropertyFilter(
77
+ name="tag",
78
+ description="Filter by tag label.",
79
+ property_key="tags",
80
+ transform=transform_extract_labels,
81
+ ),
82
+ DirectMultiValuePropertyFilter(
83
+ name="ids",
84
+ description="Internal filter for multiple dataset IDs.",
85
+ property_key="id",
86
+ ),
87
+ ],
88
+ )
90
89
 
91
90
 
92
91
  def _validate_and_split_dataset_id(dataset_id: str) -> tuple[str, str]:
@@ -105,13 +104,64 @@ def _validate_and_split_dataset_id(dataset_id: str) -> tuple[str, str]:
105
104
  project_part = dataset_id.split(":")[0]
106
105
  dataset_part = dataset_id.split(":")[1]
107
106
  except IndexError as error:
108
- raise ClickException(
107
+ raise CmemcError(
109
108
  f"{dataset_id} is not a valid dataset ID. Use the "
110
109
  "'dataset list' command to get a list of existing datasets."
111
110
  ) from error
112
111
  return project_part, dataset_part
113
112
 
114
113
 
114
+ def _validate_dataset_ids(dataset_ids: tuple[str, ...]) -> None:
115
+ """Validate that all provided dataset IDs exist."""
116
+ if not dataset_ids:
117
+ return
118
+ all_datasets = list_items(item_type="dataset")["results"]
119
+ all_dataset_ids = [_["projectId"] + ":" + _["id"] for _ in all_datasets]
120
+ for dataset_id in dataset_ids:
121
+ if dataset_id not in all_dataset_ids:
122
+ raise CmemcError(
123
+ f"Dataset {dataset_id} not available. Use the 'dataset list' "
124
+ "command to get a list of existing datasets."
125
+ )
126
+
127
+
128
+ def _get_datasets_to_delete(
129
+ ctx: Context,
130
+ dataset_ids: tuple[str, ...],
131
+ all_: bool,
132
+ filter_: tuple[tuple[str, str], ...],
133
+ ) -> list[str]:
134
+ """Get the list of dataset IDs to delete based on selection method."""
135
+ if all_:
136
+ # Get all datasets
137
+ datasets = list_items(item_type="dataset")["results"]
138
+ return [_["projectId"] + ":" + _["id"] for _ in datasets]
139
+
140
+ # Validate provided IDs exist before proceeding
141
+ _validate_dataset_ids(dataset_ids)
142
+
143
+ # Build filter list
144
+ filter_to_apply = list(filter_) if filter_ else []
145
+
146
+ # Add IDs if provided (using internal multi-value filter)
147
+ if dataset_ids:
148
+ # Extract just the dataset ID part (after the colon) for filtering
149
+ dataset_id_parts = [_.split(":")[1] for _ in dataset_ids]
150
+ filter_to_apply.append(("ids", ",".join(dataset_id_parts)))
151
+
152
+ # Apply filters
153
+ datasets = dataset_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
154
+
155
+ # Build full dataset IDs
156
+ result = [_["projectId"] + ":" + _["id"] for _ in datasets]
157
+
158
+ # Validation: ensure we found datasets
159
+ if not result and not dataset_ids:
160
+ raise CmemcError("No datasets found matching the provided filters.")
161
+
162
+ return result
163
+
164
+
115
165
  def _post_file_resource(
116
166
  app: ApplicationContext,
117
167
  project_id: str,
@@ -182,7 +232,7 @@ def _get_read_only_out_of_parameter(parameter_dict: dict) -> bool:
182
232
  return True
183
233
  if read_only in ("false", False, "False"):
184
234
  return False
185
- raise ClickException(f"readOnly parameter should be 'true' or 'false' - was {read_only!r}")
235
+ raise CmemcError(f"readOnly parameter should be 'true' or 'false' - was {read_only!r}")
186
236
 
187
237
 
188
238
  def _extend_parameter_with_metadata(
@@ -285,7 +335,12 @@ def _show_parameter_list(app: ApplicationContext, dataset_type: str) -> None:
285
335
  # metadata always on top, then sorted by key
286
336
  table = sorted(table, key=lambda k: k[0].lower())
287
337
  table = completion.add_metadata_parameter(table)
288
- app.echo_info_table(table, headers=["Parameter", "Description"])
338
+ app.echo_info_table(
339
+ table,
340
+ headers=["Parameter", "Description"],
341
+ caption=build_caption(len(table), f"{dataset_type} dataset parameter"),
342
+ empty_table_message="No parameters found for this dataset type.",
343
+ )
289
344
 
290
345
 
291
346
  def _show_type_list(app: ApplicationContext) -> None:
@@ -309,7 +364,13 @@ def _show_type_list(app: ApplicationContext) -> None:
309
364
  f"{title}: {description}",
310
365
  ]
311
366
  table.append(row)
312
- app.echo_info_table(table, headers=["Dataset Type", "Description"], sort_column=1)
367
+ app.echo_info_table(
368
+ table,
369
+ headers=["Dataset Type", "Description"],
370
+ sort_column=1,
371
+ caption=build_caption(len(table), "dataset type"),
372
+ empty_table_message="No dataset types found.",
373
+ )
313
374
 
314
375
 
315
376
  def _check_or_select_dataset_type(app: ApplicationContext, dataset_type: str) -> tuple[str, dict]:
@@ -333,7 +394,7 @@ def _check_or_select_dataset_type(app: ApplicationContext, dataset_type: str) ->
333
394
  app.echo_debug(f"check type {dataset_type}")
334
395
  plugin = get_task_plugin_description(dataset_type)
335
396
  except requests.exceptions.HTTPError as error:
336
- raise CmemcError(app, f"Unknown dataset type: {dataset_type}.") from error
397
+ raise CmemcError(f"Unknown dataset type: {dataset_type}.") from error
337
398
  else:
338
399
  return dataset_type, plugin
339
400
 
@@ -344,8 +405,8 @@ def _check_or_select_dataset_type(app: ApplicationContext, dataset_type: str) ->
344
405
  "filter_",
345
406
  type=(str, str),
346
407
  multiple=True,
347
- shell_complete=completion.dataset_list_filter,
348
- help=DATASET_LIST_FILTER_HELP_TEXT,
408
+ shell_complete=dataset_list.complete_values,
409
+ help=dataset_list.get_filter_help_text(),
349
410
  )
350
411
  @click.option(
351
412
  "--raw", is_flag=True, help="Outputs raw JSON objects of the dataset search API response."
@@ -356,19 +417,15 @@ def _check_or_select_dataset_type(app: ApplicationContext, dataset_type: str) ->
356
417
  help="Lists only dataset IDs and no labels or other metadata. "
357
418
  "This is useful for piping the IDs into other cmemc commands.",
358
419
  )
359
- @click.pass_obj
360
- def list_command(
361
- app: ApplicationContext, filter_: tuple[tuple[str, str]], raw: bool, id_only: bool
362
- ) -> None:
420
+ @click.pass_context
421
+ def list_command(ctx: Context, filter_: tuple[tuple[str, str]], raw: bool, id_only: bool) -> None:
363
422
  """List available datasets.
364
423
 
365
424
  Output and filter a list of available datasets. Each dataset is listed
366
425
  with its ID, type and label.
367
426
  """
368
- datasets = list_items(item_type="dataset")["results"]
369
- for _ in filter_:
370
- filter_type, filter_name = _
371
- datasets = _get_datasets_filtered(datasets, filter_type, filter_name)
427
+ app = ctx.obj
428
+ datasets = dataset_list.apply_filters(ctx=ctx, filter_=filter_)
372
429
 
373
430
  if raw:
374
431
  app.echo_info_json(datasets)
@@ -378,18 +435,21 @@ def list_command(
378
435
  else:
379
436
  table = []
380
437
  for _ in datasets:
381
- row = [
382
- _["projectId"] + ":" + _["id"],
383
- _["pluginId"],
384
- _["label"],
385
- ]
438
+ # Build row with dataset ID; the Label column will be transformed by DatasetLink
439
+ dataset_id = _["projectId"] + ":" + _["id"]
440
+ row = [dataset_id, _["pluginId"], dataset_id]
441
+
386
442
  table.append(row)
443
+ filtered = len(filter_) > 0
387
444
  app.echo_info_table(
388
445
  table,
389
446
  headers=["Dataset ID", "Type", "Label"],
390
447
  sort_column=2,
391
- empty_table_message="No datasets found. "
392
- "Use the `dataset create` command to create a new dataset.",
448
+ cell_processing={2: DatasetLink(), 1: DatasetTypeLink()},
449
+ caption=build_caption(len(table), "dataset", filtered=filtered),
450
+ empty_table_message="No datasets found for these filters."
451
+ if filtered
452
+ else "No datasets found. Use the `dataset create` command to create a new dataset.",
393
453
  )
394
454
 
395
455
 
@@ -401,28 +461,18 @@ def list_command(
401
461
  is_flag=True,
402
462
  help="Delete all datasets. " "This is a dangerous option, so use it with care.",
403
463
  )
404
- @click.option(
405
- "--project",
406
- "project_id",
407
- type=click.STRING,
408
- shell_complete=completion.project_ids,
409
- help="In combination with the '--all' flag, this option allows for "
410
- "deletion of all datasets of a certain project. The behaviour is "
411
- "similar to the 'dataset list --project' command.",
412
- )
413
464
  @click.option(
414
465
  "--filter",
415
466
  "filter_",
416
467
  type=(str, str),
417
468
  multiple=True,
418
- shell_complete=completion.dataset_list_filter,
419
- help=DATASET_DELETE_FILTER_HELP_TEXT,
469
+ shell_complete=dataset_list.complete_values,
470
+ help=dataset_list.get_filter_help_text(),
420
471
  )
421
472
  @click.argument("dataset_ids", nargs=-1, type=click.STRING, shell_complete=completion.dataset_ids)
422
- @click.pass_obj
473
+ @click.pass_context
423
474
  def delete_command(
424
- app: ApplicationContext,
425
- project_id: str,
475
+ ctx: Context,
426
476
  all_: bool,
427
477
  filter_: tuple[tuple[str, str]],
428
478
  dataset_ids: tuple[str],
@@ -436,12 +486,10 @@ def delete_command(
436
486
 
437
487
  Note: Datasets can be listed by using the `dataset list` command.
438
488
  """
439
- if project_id:
440
- app.echo_warning(
441
- "Option '--project' is deprecated and will be removed. "
442
- "Please use '--filter project XXX' instead."
443
- )
444
- if dataset_ids == () and not all_ and not filter_:
489
+ app = ctx.obj
490
+
491
+ # Validation: require at least one selection method
492
+ if not dataset_ids and not all_ and not filter_:
445
493
  raise UsageError(
446
494
  "Either specify at least one dataset ID"
447
495
  " or use a --filter option,"
@@ -451,25 +499,21 @@ def delete_command(
451
499
  if dataset_ids and (all_ or filter_):
452
500
  raise UsageError("Either specify a dataset ID OR" " use a --filter or the --all option.")
453
501
 
454
- if all_ or filter_:
455
- # in case --all or --filter is given, a list of datasets is fetched
456
- dataset_ids = []
457
- datasets = list_items(item_type="dataset", project=project_id)["results"]
458
- for _ in filter_:
459
- filter_type, filter_name = _
460
- datasets = _get_datasets_filtered(datasets, filter_type, filter_name)
461
- for _ in datasets:
462
- dataset_ids.append(_["projectId"] + ":" + _["id"])
502
+ # Get datasets to delete based on selection method
503
+ datasets_to_delete = _get_datasets_to_delete(ctx, dataset_ids, all_, filter_)
463
504
 
464
- count = len(dataset_ids)
465
- current = 1
466
- for _ in dataset_ids:
467
- app.echo_info(f"Delete dataset {current}/{count}: {_} ... ", nl=False)
468
- project_part, dataset_part = _validate_and_split_dataset_id(_)
505
+ # Avoid double removal as well as sort IDs
506
+ processed_ids = sorted(set(datasets_to_delete), key=lambda v: v.lower())
507
+ count = len(processed_ids)
508
+
509
+ # Delete each dataset
510
+ for current, dataset_id in enumerate(processed_ids, start=1):
511
+ current_string = str(current).zfill(len(str(count)))
512
+ app.echo_info(f"Delete dataset {current_string}/{count}: {dataset_id} ... ", nl=False)
513
+ project_part, dataset_part = _validate_and_split_dataset_id(dataset_id)
469
514
  app.echo_debug(f"Project ID is {project_part}, dataset ID is {dataset_part}")
470
515
  delete_dataset(project_part, dataset_part)
471
- app.echo_success("done")
472
- current = current + 1
516
+ app.echo_success("deleted")
473
517
 
474
518
 
475
519
  @click.command(cls=CmemcCommand, name="download")
@@ -511,7 +555,7 @@ def download_command(
511
555
  file = project["data"]["parameters"]["file"]
512
556
  except KeyError as no_file_resource:
513
557
  raise CmemcError(
514
- app, f"The dataset {dataset_id} has no associated file resource."
558
+ f"The dataset {dataset_id} has no associated file resource."
515
559
  ) from no_file_resource
516
560
  if Path(output_path).exists() and replace is not True:
517
561
  raise UsageError(
@@ -844,7 +888,7 @@ def open_command(app: ApplicationContext, dataset_ids: tuple[str]) -> None:
844
888
  app.echo_debug(f"Open {_}: {full_url}")
845
889
  click.launch(full_url)
846
890
  else:
847
- raise ClickException(f"Dataset '{_}' not found.")
891
+ raise CmemcError(f"Dataset '{_}' not found.")
848
892
 
849
893
 
850
894
  @click.group(cls=CmemcGroup)