cmem-cmemc 24.3.3__py3-none-any.whl → 25.1.1__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 (42) hide show
  1. cmem_cmemc/__init__.py +1 -160
  2. cmem_cmemc/cli.py +138 -0
  3. cmem_cmemc/command.py +36 -0
  4. cmem_cmemc/commands/admin.py +7 -6
  5. cmem_cmemc/commands/client.py +4 -3
  6. cmem_cmemc/commands/config.py +3 -2
  7. cmem_cmemc/commands/dataset.py +18 -17
  8. cmem_cmemc/commands/graph.py +20 -22
  9. cmem_cmemc/commands/manual.py +56 -0
  10. cmem_cmemc/commands/metrics.py +11 -11
  11. cmem_cmemc/commands/migration.py +4 -3
  12. cmem_cmemc/commands/project.py +10 -10
  13. cmem_cmemc/commands/python.py +29 -2
  14. cmem_cmemc/commands/query.py +31 -14
  15. cmem_cmemc/commands/resource.py +4 -3
  16. cmem_cmemc/commands/scheduler.py +4 -4
  17. cmem_cmemc/commands/store.py +2 -2
  18. cmem_cmemc/commands/user.py +10 -9
  19. cmem_cmemc/commands/validation.py +2 -2
  20. cmem_cmemc/commands/variable.py +1 -1
  21. cmem_cmemc/commands/vocabulary.py +11 -10
  22. cmem_cmemc/commands/workflow.py +18 -18
  23. cmem_cmemc/completion.py +76 -49
  24. cmem_cmemc/config_parser.py +44 -0
  25. cmem_cmemc/context.py +168 -99
  26. cmem_cmemc/exceptions.py +15 -2
  27. cmem_cmemc/manual_helper/graph.py +2 -0
  28. cmem_cmemc/manual_helper/multi_page.py +1 -1
  29. cmem_cmemc/manual_helper/single_page.py +2 -0
  30. cmem_cmemc/migrations/remove_noop_triple_251.py +50 -0
  31. cmem_cmemc/migrations/shapes_widget_integrations_243.py +0 -5
  32. cmem_cmemc/object_list.py +3 -3
  33. cmem_cmemc/parameter_types/path.py +7 -0
  34. cmem_cmemc/placeholder.py +69 -0
  35. cmem_cmemc/utils.py +49 -15
  36. {cmem_cmemc-24.3.3.dist-info → cmem_cmemc-25.1.1.dist-info}/METADATA +14 -14
  37. cmem_cmemc-25.1.1.dist-info/RECORD +59 -0
  38. cmem_cmemc-25.1.1.dist-info/entry_points.txt +3 -0
  39. cmem_cmemc-24.3.3.dist-info/RECORD +0 -54
  40. cmem_cmemc-24.3.3.dist-info/entry_points.txt +0 -3
  41. {cmem_cmemc-24.3.3.dist-info → cmem_cmemc-25.1.1.dist-info}/LICENSE +0 -0
  42. {cmem_cmemc-24.3.3.dist-info → cmem_cmemc-25.1.1.dist-info}/WHEEL +0 -0
@@ -14,7 +14,7 @@ from xml.etree.ElementTree import ( # nosec
14
14
  )
15
15
 
16
16
  import click
17
- from click import Argument
17
+ from click import Argument, ClickException, UsageError
18
18
  from cmem.cmempy.config import get_cmem_base_uri
19
19
  from cmem.cmempy.dp.authorization import refresh
20
20
  from cmem.cmempy.dp.proxy import graph as graph_api
@@ -121,7 +121,7 @@ def _get_export_names(
121
121
 
122
122
  Raises:
123
123
  ------
124
- ValueError in case the template string produces a naming clash,
124
+ ClickException in case the template string produces a naming clash,
125
125
  means two IRIs result in the same filename
126
126
 
127
127
  """
@@ -135,7 +135,7 @@ def _get_export_names(
135
135
  _name_created = f"{Template(template).render(template_data)}{file_extension}"
136
136
  _names[iri] = _name_created
137
137
  if len(_names.values()) != len(set(_names.values())):
138
- raise ValueError(
138
+ raise ClickException(
139
139
  "The given template string produces a naming clash. "
140
140
  "Please use a different template to produce unique names."
141
141
  )
@@ -208,7 +208,7 @@ def _get_graphs_filtered(filter_name: str, filter_value: str) -> list[dict]:
208
208
  # check for correct filter names
209
209
  possible_filter_names = ("access", "imported-by")
210
210
  if filter_name not in possible_filter_names:
211
- raise ValueError(
211
+ raise ClickException(
212
212
  f"{filter_name} is an unknown filter name. " f"Use one of {possible_filter_names}."
213
213
  )
214
214
  # filter by access condition
@@ -218,14 +218,14 @@ def _get_graphs_filtered(filter_name: str, filter_value: str) -> list[dict]:
218
218
  elif filter_value == "readonly":
219
219
  graphs = get_graphs(writeable=False, readonly=True)
220
220
  else:
221
- raise ValueError("Filter access is either 'readonly' or 'writeable'.")
221
+ raise ClickException("Filter access is either 'readonly' or 'writeable'.")
222
222
  else:
223
223
  # default is all graphs
224
224
  graphs = get_graphs()
225
225
  # filter by imported-by
226
226
  if filter_name == "imported-by":
227
227
  if filter_value not in get_graphs_as_dict():
228
- raise ValueError(UNKNOWN_GRAPH_ERROR.format(filter_value))
228
+ raise ClickException(UNKNOWN_GRAPH_ERROR.format(filter_value))
229
229
  imported_graphs = get_graph_imports(filter_value)
230
230
  graphs = [_ for _ in graphs if _["iri"] in imported_graphs]
231
231
  return graphs
@@ -271,19 +271,19 @@ def _check_and_extend_exported_graphs(
271
271
 
272
272
  Raises:
273
273
  ------
274
- ValueError
274
+ UsageError or ClickException
275
275
 
276
276
  """
277
277
  # transform given IRI-tuple to a distinct IRI-list
278
278
  iris = list(set(iris))
279
279
  if len(iris) == 0 and not all_flag:
280
- raise ValueError(
280
+ raise UsageError(
281
281
  "Either provide at least one graph IRI or use the --all option "
282
282
  "in order to work with all graphs."
283
283
  )
284
284
  for iri in iris:
285
285
  if iri not in all_graphs:
286
- raise ValueError(UNKNOWN_GRAPH_ERROR.format(iri))
286
+ raise ClickException(UNKNOWN_GRAPH_ERROR.format(iri))
287
287
  if all_flag:
288
288
  # in case --all is given,
289
289
  # list of graphs is filled with all available graph IRIs
@@ -377,7 +377,7 @@ def tree_command(
377
377
  """
378
378
  graphs = get_graphs_as_dict()
379
379
  if not iris and not all_:
380
- raise ValueError(
380
+ raise UsageError(
381
381
  "Either specify at least one graph IRI or use the "
382
382
  "--all option to show the owl:imports tree of all graphs."
383
383
  )
@@ -386,7 +386,7 @@ def tree_command(
386
386
 
387
387
  for iri in iris:
388
388
  if iri not in graphs:
389
- raise ValueError(UNKNOWN_GRAPH_ERROR.format(iri))
389
+ raise ClickException(UNKNOWN_GRAPH_ERROR.format(iri))
390
390
 
391
391
  iris = sorted(iris, key=lambda x: graphs[x]["label"]["title"].lower())
392
392
 
@@ -413,7 +413,7 @@ def tree_command(
413
413
  output += os.linesep.join(
414
414
  [
415
415
  line
416
- for line in tree.show(key=lambda x: x.tag.lower(), stdout=False).splitlines()
416
+ for line in tree.show(key=lambda x: x.tag.lower(), stdout=False).splitlines() # type: ignore[arg-type, return-value]
417
417
  if line.strip()
418
418
  ]
419
419
  )
@@ -486,12 +486,12 @@ def _validate_export_command_input_parameters(
486
486
  ) -> None:
487
487
  """Validate export command input parameters combinations"""
488
488
  if output_dir and create_catalog and compress:
489
- raise click.UsageError(
489
+ raise UsageError(
490
490
  "Cannot create a catalog file when using a compressed graph file."
491
491
  " Please remove either the --create-catalog or --compress option."
492
492
  )
493
493
  if output_file == "- " and compress:
494
- raise click.UsageError("Cannot output a binary file to terminal. Use --output-file option.")
494
+ raise UsageError("Cannot output a binary file to terminal. Use --output-file option.")
495
495
 
496
496
 
497
497
  # pylint: disable=too-many-arguments,too-many-locals
@@ -618,9 +618,7 @@ def export_command( # noqa: PLR0913
618
618
  # no output directory set -> file export
619
619
  if output_file == "-":
620
620
  if compress:
621
- raise click.UsageError(
622
- "Cannot output a binary file to terminal. Use --output-file option."
623
- )
621
+ raise UsageError("Cannot output a binary file to terminal. Use --output-file option.")
624
622
  # in case a file is stdout,
625
623
  # all triples from all graphs go in and other output is suppressed
626
624
  app.echo_debug("output is stdout")
@@ -647,7 +645,7 @@ def validate_input_path(input_path: str) -> None:
647
645
  """Validate input path
648
646
 
649
647
  This function checks the provided folder for any .ttl or .nt files
650
- that have corresponding .gz files. If such files are found, it raises a ValueError.
648
+ that have corresponding .gz files. If such files are found, it raises a ClickException.
651
649
  """
652
650
  files = os.listdir(input_path)
653
651
 
@@ -659,7 +657,7 @@ def validate_input_path(input_path: str) -> None:
659
657
  conflicting_files = [f for f in gz_files if f in files]
660
658
 
661
659
  if conflicting_files:
662
- raise ValueError(
660
+ raise ClickException(
663
661
  f"The following RDF files (.ttl/.nt) have corresponding '.gz' files,"
664
662
  f" which is not allowed: {', '.join(conflicting_files)}"
665
663
  )
@@ -755,7 +753,7 @@ def import_command(
755
753
  Note: Directories are scanned on the first level only (not recursively).
756
754
  """
757
755
  if replace and skip_existing:
758
- raise ValueError(
756
+ raise UsageError(
759
757
  "The options --replace and --skip-existing are mutually "
760
758
  "exclusive, so please remove one of them."
761
759
  )
@@ -779,7 +777,7 @@ def import_command(
779
777
 
780
778
  elif SmartPath(input_path).is_file():
781
779
  if iri is None:
782
- raise ValueError(
780
+ raise UsageError(
783
781
  "Either specify an input file AND a graph IRI or an input directory ONLY."
784
782
  )
785
783
  graphs = [(input_path, iri)]
@@ -878,7 +876,7 @@ def count_command(
878
876
  Counts do not include imported graphs.
879
877
  """
880
878
  if not iris and not all_:
881
- raise ValueError(
879
+ raise UsageError(
882
880
  "Either specify at least one graph IRI " "or use the --all option to count all graphs."
883
881
  )
884
882
  if all_:
@@ -0,0 +1,56 @@
1
+ """manual command for cmem command line interface"""
2
+
3
+ import click
4
+ from click import Context, UsageError
5
+
6
+ from cmem_cmemc.command import CmemcCommand
7
+ from cmem_cmemc.manual_helper.graph import print_manual_graph
8
+ from cmem_cmemc.manual_helper.multi_page import create_multi_page_documentation
9
+ from cmem_cmemc.manual_helper.single_page import print_manual
10
+ from cmem_cmemc.parameter_types.path import ClickSmartPath
11
+ from cmem_cmemc.utils import get_version
12
+
13
+ FORMAT_MD_FILE = "markdown-single-page"
14
+ FORMAT_MD_DIRECTORY = "markdown-multi-page"
15
+ FORMAT_TURTLE_FILE = "turtle"
16
+
17
+
18
+ @click.command(cls=CmemcCommand, name="manual", hidden=True)
19
+ @click.option(
20
+ "--format",
21
+ "format_",
22
+ type=click.Choice([FORMAT_MD_FILE, FORMAT_MD_DIRECTORY, FORMAT_TURTLE_FILE]),
23
+ help=f"Output format. The '{FORMAT_TURTLE_FILE}' and '{FORMAT_MD_FILE}' formats will "
24
+ f"be returned to stdout. The '{FORMAT_MD_DIRECTORY}' format creates a directory tree "
25
+ f"(use '--output-dir' to specify the root directory)",
26
+ )
27
+ @click.option(
28
+ "--output-dir",
29
+ type=ClickSmartPath(writable=True, file_okay=False),
30
+ help=f"The output directory to create the '{FORMAT_MD_DIRECTORY}' documentation. "
31
+ f"Warning: Existing directories will be overwritten.",
32
+ )
33
+ @click.pass_context
34
+ def manual_command(ctx: Context, format_: str, output_dir: str) -> None:
35
+ """Generate reference documentation assets.
36
+
37
+ This command generates the cmemc reference documentation from the help texts
38
+ and outputs it in different formats.
39
+ """
40
+ if format_ is None:
41
+ raise UsageError(
42
+ "Please use the --format option to specify an output format. "
43
+ "Use --help for more information."
44
+ )
45
+ if format_ == FORMAT_TURTLE_FILE:
46
+ print_manual_graph(ctx.find_root(), get_version())
47
+ return
48
+ if format_ == FORMAT_MD_FILE:
49
+ print_manual(ctx.find_root())
50
+ return
51
+ if output_dir is None:
52
+ raise UsageError(
53
+ f"The output format '{FORMAT_MD_DIRECTORY}' needs an output directory "
54
+ f"('--output-dir'). Use --help for more information."
55
+ )
56
+ create_multi_page_documentation(ctx.find_root(), str(output_dir))
@@ -1,7 +1,7 @@
1
1
  """metrics commands for cmem command line interface."""
2
2
 
3
3
  import click
4
- from click import Argument, Context
4
+ from click import Argument, ClickException, Context, UsageError
5
5
  from click.shell_completion import CompletionItem
6
6
  from cmem.cmempy.api import request
7
7
  from cmem.cmempy.config import get_cmem_base_uri, get_di_api_endpoint, get_dp_api_endpoint
@@ -11,7 +11,7 @@ from requests import HTTPError
11
11
  from cmem_cmemc import completion
12
12
  from cmem_cmemc.command import CmemcCommand
13
13
  from cmem_cmemc.command_group import CmemcGroup
14
- from cmem_cmemc.context import CONTEXT, ApplicationContext
14
+ from cmem_cmemc.context import ApplicationContext
15
15
  from cmem_cmemc.object_list import (
16
16
  DirectValuePropertyFilter,
17
17
  ObjectList,
@@ -96,7 +96,7 @@ metrics_list = ObjectList(
96
96
 
97
97
  def _complete_metrics_id(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]: # noqa: ARG001
98
98
  """Prepare a list of metric identifier."""
99
- CONTEXT.set_connection_from_params(ctx.find_root().params)
99
+ ApplicationContext.set_connection_from_params(ctx.find_root().params)
100
100
  candidates = [(_["id"], _["documentation"]) for _ in metrics_list.apply_filters(ctx=ctx)]
101
101
  return completion.finalize_completion(candidates=candidates, incomplete=incomplete)
102
102
 
@@ -107,7 +107,7 @@ def _complete_metric_label_filter(
107
107
  incomplete: str,
108
108
  ) -> list[CompletionItem]:
109
109
  """Prepare a list of label name or values"""
110
- CONTEXT.set_connection_from_params(ctx.find_root().params)
110
+ ApplicationContext.set_connection_from_params(ctx.find_root().params)
111
111
  args = completion.get_completion_args(incomplete)
112
112
  incomplete = incomplete.lower()
113
113
  options: list[str] = []
@@ -141,11 +141,11 @@ def _filter_samples(family: dict, label_filter: tuple[tuple[str, str], ...]) ->
141
141
  sample_labels = sample[1]
142
142
  for name, value in label_filter:
143
143
  if name not in labels:
144
- raise ValueError(
144
+ raise ClickException(
145
145
  f"The metric '{family_name}' does " f"not have a label named '{name}'."
146
146
  )
147
147
  if value not in labels[name]:
148
- raise ValueError(
148
+ raise ClickException(
149
149
  f"The metric '{family_name}' does "
150
150
  f"not have a label '{name}' with the value '{value}'."
151
151
  )
@@ -200,12 +200,12 @@ def get_command(
200
200
  app = ctx.obj
201
201
  data = metrics_list.apply_filters(ctx=ctx, filter_=[("id", metric_id)])
202
202
  if len(data) == 0:
203
- raise click.UsageError(
203
+ raise UsageError(
204
204
  f"No metric with ID '{metric_id}' found. "
205
205
  "Use the `metrics list` command to list all metrics."
206
206
  )
207
207
  if len(data) > 1:
208
- raise click.UsageError("Unknown Error - More than one metric with ID '{metric_id}' found.")
208
+ raise UsageError("Unknown Error - More than one metric with ID '{metric_id}' found.")
209
209
  metric = data[0]
210
210
 
211
211
  samples = _filter_samples(metric, label_filter)
@@ -214,7 +214,7 @@ def get_command(
214
214
  return
215
215
 
216
216
  if len(samples) == 0:
217
- raise ValueError(
217
+ raise ClickException(
218
218
  "No data - the given label combination filtered out "
219
219
  f"all available samples of the metric {metric_id}."
220
220
  )
@@ -248,12 +248,12 @@ def inspect_command(ctx: Context, metric_id: str, raw: bool) -> None:
248
248
  app = ctx.obj
249
249
  data = metrics_list.apply_filters(ctx=ctx, filter_=[("id", metric_id)])
250
250
  if len(data) == 0:
251
- raise click.UsageError(
251
+ raise UsageError(
252
252
  f"No metric with ID '{metric_id}' found. "
253
253
  "Use the `metrics list` command to list all metrics."
254
254
  )
255
255
  if len(data) > 1:
256
- raise click.UsageError("Unknown Error - More than one metric with ID '{metric_id}' found.")
256
+ raise UsageError("Unknown Error - More than one metric with ID '{metric_id}' found.")
257
257
  if raw:
258
258
  app.echo_info_json(data)
259
259
  return
@@ -9,12 +9,13 @@ from click.shell_completion import CompletionItem
9
9
  from cmem_cmemc.command import CmemcCommand
10
10
  from cmem_cmemc.command_group import CmemcGroup
11
11
  from cmem_cmemc.completion import check_option_in_params, finalize_completion
12
- from cmem_cmemc.context import CONTEXT
12
+ from cmem_cmemc.context import ApplicationContext
13
13
  from cmem_cmemc.migrations.access_conditions_243 import (
14
14
  MoveAccessConditionsToNewGraph,
15
15
  RenameAuthVocabularyResources,
16
16
  )
17
17
  from cmem_cmemc.migrations.bootstrap_data import MigrateBootstrapData
18
+ from cmem_cmemc.migrations.remove_noop_triple_251 import RemoveHideHeaderFooterStatements
18
19
  from cmem_cmemc.migrations.shapes_widget_integrations_243 import (
19
20
  ChartsOnNodeShapesToToWidgetIntegrations,
20
21
  ChartsOnPropertyShapesToWidgetIntegrations,
@@ -32,7 +33,6 @@ from cmem_cmemc.object_list import (
32
33
  )
33
34
 
34
35
  if TYPE_CHECKING:
35
- from cmem_cmemc.context import ApplicationContext
36
36
  from cmem_cmemc.migrations.abc import MigrationRecipe
37
37
 
38
38
 
@@ -58,6 +58,7 @@ def get_migrations(ctx: click.Context) -> list[dict]: # noqa: ARG001
58
58
  WorkflowTriggerPropertyShapesToWidgetIntegrations(),
59
59
  TableReportPropertyShapesToWidgetIntegrations(),
60
60
  SparqlDatatypesToXsdString(),
61
+ RemoveHideHeaderFooterStatements(),
61
62
  ]
62
63
  ]
63
64
  data.sort(key=lambda x: x["first_version"])
@@ -66,7 +67,7 @@ def get_migrations(ctx: click.Context) -> list[dict]: # noqa: ARG001
66
67
 
67
68
  def complete_migration_ids(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]:
68
69
  """Prepare a list of migration recipe IDs"""
69
- CONTEXT.set_connection_from_params(ctx.find_root().params)
70
+ ApplicationContext.set_connection_from_params(ctx.find_root().params)
70
71
  options = []
71
72
  for _ in get_migrations(ctx=ctx):
72
73
  id_ = _["id"]
@@ -7,7 +7,7 @@ import tempfile
7
7
  from zipfile import ZipFile
8
8
 
9
9
  import click
10
- from click import UsageError
10
+ from click import ClickException, UsageError
11
11
  from cmem.cmempy.config import get_di_api_endpoint
12
12
  from cmem.cmempy.plugins.marshalling import (
13
13
  get_extension_by_plugin,
@@ -44,7 +44,7 @@ def _validate_projects_to_process(project_ids: tuple[str], all_flag: bool) -> li
44
44
  list of IDs is without duplicates, and validated if they exist
45
45
  """
46
46
  if project_ids == () and not all_flag:
47
- raise ValueError(
47
+ raise UsageError(
48
48
  "Either specify at least one project ID "
49
49
  "or use the --all option to process over all projects."
50
50
  )
@@ -59,7 +59,7 @@ def _validate_projects_to_process(project_ids: tuple[str], all_flag: bool) -> li
59
59
  # test if one of the projects does NOT exist
60
60
  for _ in projects_to_process:
61
61
  if _ not in all_projects:
62
- raise ValueError(f"Project {_} does not exist.")
62
+ raise ClickException(f"Project {_} does not exist.")
63
63
  return projects_to_process
64
64
 
65
65
 
@@ -104,7 +104,7 @@ def open_command(app: ApplicationContext, project_ids: tuple[str]) -> None:
104
104
  projects = get_projects()
105
105
  for _ in project_ids:
106
106
  if _ not in (p["name"] for p in projects):
107
- raise ValueError(f"Project '{_}' not found.")
107
+ raise ClickException(f"Project '{_}' not found.")
108
108
  open_project_uri = f"{get_di_api_endpoint()}/workbench/projects/{_}"
109
109
  app.echo_debug(f"Open {_}: {open_project_uri}")
110
110
  click.launch(open_project_uri)
@@ -221,7 +221,7 @@ def create_command(
221
221
  Note: Projects can be listed by using the `project list` command.
222
222
  """
223
223
  if from_transformation and len(project_ids) > 1:
224
- raise ValueError(
224
+ raise UsageError(
225
225
  "By using --from-transformation,"
226
226
  " the project ID parameter is limited to a single project ID."
227
227
  )
@@ -229,7 +229,7 @@ def create_command(
229
229
  all_projects = [_["name"] for _ in get_projects()]
230
230
  for project_id in project_ids:
231
231
  if project_id in all_projects:
232
- raise ValueError(f"Project {project_id} already exists.")
232
+ raise ClickException(f"Project {project_id} already exists.")
233
233
 
234
234
  if from_transformation:
235
235
  transformation_parts = from_transformation.split(":")
@@ -385,7 +385,7 @@ def export_command( # noqa: PLR0913
385
385
 
386
386
  extractable_types = ("xmlZip", "xmlZipWithoutResources")
387
387
  if extract and marshalling_plugin not in extractable_types:
388
- raise ValueError(
388
+ raise UsageError(
389
389
  f"The export type {marshalling_plugin} can not be extracted. "
390
390
  f"Use one of {extractable_types}."
391
391
  )
@@ -480,12 +480,12 @@ def import_command(
480
480
 
481
481
  all_projects = get_projects()
482
482
  if project_id and not replace and project_id in ([_["name"] for _ in all_projects]):
483
- raise ValueError(f"Project {project_id} is already there.")
483
+ raise ClickException(f"Project {project_id} is already there.")
484
484
 
485
485
  if Path(path).is_dir():
486
486
  if not (Path(path) / "config.xml").is_file():
487
487
  # fail early if directory is not an export
488
- raise ValueError(f"Directory {path} seems not to be a export directory.")
488
+ raise ClickException(f"Directory {path} seems not to be a export directory.")
489
489
 
490
490
  app.echo_info(f"Import directory {path} to project {project_id} ... ", nl=False)
491
491
  # in case of a directory, we zip it to a temp file
@@ -512,7 +512,7 @@ def import_command(
512
512
  # Remove the temporary file
513
513
  pathlib.Path.unlink(pathlib.Path(uploaded_file))
514
514
  if "errorMessage" in validation_response:
515
- raise ValueError(validation_response["errorMessage"])
515
+ raise ClickException(validation_response["errorMessage"])
516
516
  import_id = validation_response["projectImportId"]
517
517
 
518
518
  # get project_id from response if not given as parameter
@@ -5,7 +5,7 @@ from dataclasses import asdict
5
5
  from re import match
6
6
 
7
7
  import click
8
- from click import UsageError
8
+ from click import ClickException, UsageError
9
9
  from cmem.cmempy.workspace.python import (
10
10
  install_package_by_file,
11
11
  install_package_by_name,
@@ -60,7 +60,7 @@ def install_command(app: ApplicationContext, package: str) -> None:
60
60
  install_response = install_package_by_file(package_file=package)
61
61
  except FileNotFoundError as not_found_error:
62
62
  if not _looks_like_a_package(package):
63
- raise ValueError(
63
+ raise ClickException(
64
64
  f"{package} does not look like a package name or requirement "
65
65
  "string, and a file with this name also does not exists."
66
66
  ) from not_found_error
@@ -231,6 +231,12 @@ def list_plugins_command(
231
231
  except TypeError:
232
232
  plugins = raw_output # DI <= 22.1 output
233
233
 
234
+ if not all(plugin.get("isRegistered", True) for plugin in plugins):
235
+ app.echo_warning(
236
+ "Some plugins are installed but not registered. "
237
+ "Use the 'admin workspace python reload' command to register all installed plugins."
238
+ )
239
+
234
240
  if raw:
235
241
  app.echo_info_json(plugins)
236
242
  return
@@ -286,6 +292,26 @@ def open_command(app: ApplicationContext, package: str) -> None:
286
292
  click.launch(full_url)
287
293
 
288
294
 
295
+ @click.command(cls=CmemcCommand, name="reload")
296
+ @click.pass_obj
297
+ def reload_command(app: ApplicationContext) -> None:
298
+ """Reload / Register all installed plugins.
299
+
300
+ This command will register all installed plugins into the DataIntegration workspace.
301
+ This command is useful, when you are installing packages
302
+ into the DataIntegration Python environment without using the provided cmemc
303
+ commands (e.g. by mounting a prepared filesystem in the docker container).
304
+ """
305
+ app.echo_info("Reloading python packages ... ", nl=False)
306
+ update_plugin_response = update_plugins()
307
+ app.echo_debug(f"Updated Plugins: {update_plugin_response!s}")
308
+ update_errors = update_plugin_response.get("errors", [])
309
+ if len(update_errors) == 0:
310
+ app.echo_success("done")
311
+ else:
312
+ app.echo_error("error (use --debug for more information)")
313
+
314
+
289
315
  @click.group(cls=CmemcGroup)
290
316
  def python() -> CmemcGroup: # type: ignore[empty-body]
291
317
  """List, install, or uninstall python packages.
@@ -304,3 +330,4 @@ python.add_command(uninstall_command)
304
330
  python.add_command(list_command)
305
331
  python.add_command(list_plugins_command)
306
332
  python.add_command(open_command)
333
+ python.add_command(reload_command)
@@ -1,5 +1,6 @@
1
1
  """query commands for cmem command line interface."""
2
2
 
3
+ import csv
3
4
  import json
4
5
  import sys
5
6
  from hashlib import sha1
@@ -9,7 +10,7 @@ from time import sleep, time
9
10
  from uuid import uuid4
10
11
 
11
12
  import click
12
- from click import UsageError
13
+ from click import ClickException, UsageError
13
14
  from click.shell_completion import CompletionItem
14
15
  from cmem.cmempy.queries import (
15
16
  QueryCatalog,
@@ -90,12 +91,14 @@ class ReplayStatistics:
90
91
  iri = query_["iri"]
91
92
  catalog_entry = self.catalog.get_query(iri)
92
93
  if catalog_entry is None:
93
- raise ValueError(f"measure_query - query {iri} is not in catalog.")
94
+ raise ClickException(f"measure_query - query {iri} is not in catalog.")
94
95
  return catalog_entry
95
96
  query_string = query_["queryString"]
96
97
  return SparqlQuery(text=query_string)
97
98
  except KeyError as error:
98
- raise ValueError("measure_query - given input dict has no queryString key.") from error
99
+ raise ClickException(
100
+ "measure_query - given input dict has no queryString key."
101
+ ) from error
99
102
 
100
103
  def _update_statistic_on_success(self, duration: int) -> None:
101
104
  """Update statistics and counters."""
@@ -368,11 +371,9 @@ def list_command(app: ApplicationContext, id_only: bool) -> None:
368
371
  "--accept",
369
372
  default="default",
370
373
  show_default=True,
374
+ shell_complete=completion.sparql_accept_types,
371
375
  help="Accept header for the HTTP request(s). Setting this to 'default' "
372
- "means that cmemc uses an appropriate accept header for terminal "
373
- "output (text/csv for tables, text/turtle for graphs, * otherwise). "
374
- "Please refer to the Corporate Memory system manual for a list "
375
- "of accepted mime types.",
376
+ "means that cmemc uses an appropriate output for terminals.",
376
377
  )
377
378
  @click.option(
378
379
  "--no-imports",
@@ -445,16 +446,16 @@ def execute_command( # noqa: PLR0913
445
446
  placeholder = {}
446
447
  for key, value in parameter:
447
448
  if key in placeholder:
448
- raise ValueError(
449
+ raise click.UsageError(
449
450
  "Parameter can be given only once, " f"Value for '{key}' was given twice."
450
451
  )
451
452
  placeholder[key] = value
452
453
  app.echo_debug("Parameter: " + str(placeholder))
453
454
  for file_or_uri in queries:
454
455
  app.echo_debug(f"Start of execution: {file_or_uri} with " f"placeholder {placeholder}")
455
- executed_query = QueryCatalog().get_query(file_or_uri, placeholder=placeholder)
456
+ executed_query: SparqlQuery = QueryCatalog().get_query(file_or_uri, placeholder=placeholder)
456
457
  if executed_query is None:
457
- raise ValueError(f"{file_or_uri} is neither a (readable) file nor a query URI.")
458
+ raise ClickException(f"{file_or_uri} is neither a (readable) file nor a query URI.")
458
459
  app.echo_debug(
459
460
  f"Execute ({executed_query.query_type}): "
460
461
  f"{executed_query.label} < {executed_query.url}"
@@ -475,7 +476,23 @@ def execute_command( # noqa: PLR0913
475
476
  offset=offset,
476
477
  timeout=timeout,
477
478
  )
478
- app.echo_result(results)
479
+ if accept == "default" and submitted_accept == "text/csv":
480
+ csv_reader = csv.reader(results.splitlines(), delimiter=",", quotechar='"')
481
+ table = []
482
+ headers = []
483
+ for index, row in enumerate(csv_reader):
484
+ if index == 0:
485
+ headers = row
486
+ else:
487
+ table.append(row)
488
+ app.echo_info_table(
489
+ table,
490
+ headers=headers,
491
+ empty_table_message="No results for this query.",
492
+ caption=f"Query results: {executed_query.label}",
493
+ )
494
+ else:
495
+ app.echo_result(results)
479
496
 
480
497
 
481
498
  @click.command(cls=CmemcCommand, name="open")
@@ -497,7 +514,7 @@ def open_command(app: ApplicationContext, queries: tuple[str, ...]) -> None:
497
514
  for file_or_uri in queries:
498
515
  opened_query = QueryCatalog().get_query(file_or_uri)
499
516
  if opened_query is None:
500
- raise ValueError(f"{file_or_uri} is neither a (readable) file nor a query URI.")
517
+ raise ClickException(f"{file_or_uri} is neither a (readable) file nor a query URI.")
501
518
  open_query_uri = opened_query.get_editor_url()
502
519
  app.echo_debug(f"Open {file_or_uri}: {open_query_uri}")
503
520
  click.launch(open_query_uri)
@@ -653,9 +670,9 @@ def replay_command( # noqa: PLR0913
653
670
  with Path(replay_file).open(encoding="utf8") as _:
654
671
  input_queries = load(_)
655
672
  except JSONDecodeError as error:
656
- raise ValueError(f"File {replay_file} is not a valid JSON document.") from error
673
+ raise ClickException(f"File {replay_file} is not a valid JSON document.") from error
657
674
  if len(input_queries) == 0:
658
- raise ValueError(f"File {replay_file} contains no queries.")
675
+ raise ClickException(f"File {replay_file} contains no queries.")
659
676
  app.echo_debug(f"File {replay_file} contains {len(input_queries)} queries.")
660
677
 
661
678
  statistic = ReplayStatistics(app=app, label=run_label)
@@ -3,6 +3,7 @@
3
3
  import re
4
4
 
5
5
  import click
6
+ from click import ClickException, UsageError
6
7
  from cmem.cmempy.workspace.projects.resources import get_all_resources
7
8
  from cmem.cmempy.workspace.projects.resources.resource import (
8
9
  delete_resource,
@@ -32,7 +33,7 @@ def _get_resources_filtered(
32
33
  """Get file resources but filtered according to name and value."""
33
34
  # check for correct filter names (filter ids is used internally only)
34
35
  if filter_name not in RESOURCE_FILTER_TYPES + RESOURCE_FILTER_TYPES_HIDDEN:
35
- raise ValueError(
36
+ raise UsageError(
36
37
  f"{filter_name} is an unknown filter name. " f"Use one of {RESOURCE_FILTER_TYPES}."
37
38
  )
38
39
  # filter by ID list
@@ -136,7 +137,7 @@ def delete_command(
136
137
  all resources.
137
138
  """
138
139
  if resource_ids == () and not all_ and filters_ == ():
139
- raise ValueError(
140
+ raise UsageError(
140
141
  "Either specify at least one resource ID or use the --all or "
141
142
  "--filter options to specify resources for deletion."
142
143
  )
@@ -145,7 +146,7 @@ def delete_command(
145
146
  if len(resource_ids) > 0:
146
147
  for resource_id in resource_ids:
147
148
  if resource_id not in [_["id"] for _ in resources]:
148
- raise ValueError(f"Resource {resource_id} not available.")
149
+ raise ClickException(f"Resource {resource_id} not available.")
149
150
  # "filter" by id
150
151
  resources = _get_resources_filtered(resources, "ids", resource_ids)
151
152
  for _ in filters_: