cmem-cmemc 24.1.5__py3-none-any.whl → 24.2.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.
@@ -4,7 +4,6 @@ import json
4
4
  import sys
5
5
  from hashlib import sha1
6
6
  from json import JSONDecodeError, load
7
- from pathlib import Path
8
7
  from shutil import get_terminal_size
9
8
  from time import sleep, time
10
9
  from uuid import uuid4
@@ -12,7 +11,12 @@ from uuid import uuid4
12
11
  import click
13
12
  from click import UsageError
14
13
  from click.shell_completion import CompletionItem
15
- from cmem.cmempy.queries import QUERY_CATALOG, SparqlQuery, cancel_query, get_query_status
14
+ from cmem.cmempy.queries import (
15
+ QueryCatalog,
16
+ SparqlQuery,
17
+ cancel_query,
18
+ get_query_status,
19
+ )
16
20
  from requests import HTTPError
17
21
 
18
22
  from cmem_cmemc import completion
@@ -26,6 +30,8 @@ from cmem_cmemc.object_list import (
26
30
  compare_int_greater_than,
27
31
  compare_regex,
28
32
  )
33
+ from cmem_cmemc.parameter_types.path import ClickSmartPath
34
+ from cmem_cmemc.smart_path import SmartPath as Path
29
35
  from cmem_cmemc.utils import extract_error_message, struct_to_table
30
36
 
31
37
  QUERY_FILTER_TYPES = sorted(["graph", "status", "slower-than", "type", "regex", "trace-id", "user"])
@@ -52,7 +58,7 @@ class ReplayStatistics:
52
58
  query_count: int = 0
53
59
  error_count: int = 0
54
60
  query_average: float
55
- catalog = QUERY_CATALOG
61
+ catalog = QueryCatalog()
56
62
  app: ApplicationContext
57
63
 
58
64
  def __init__(self, app: ApplicationContext, label: str | None = None):
@@ -162,11 +168,12 @@ class ReplayStatistics:
162
168
 
163
169
  def create_output(self) -> dict:
164
170
  """Create the structure for the output commands."""
165
- output = {}
166
- for key, value in vars(self).items():
167
- if key not in ("current_loop_key", "app"):
168
- # ignore some object vars on output
169
- output[key] = value
171
+ # create dict from object but ignore some internal vars on output
172
+ output = {
173
+ key: value
174
+ for (key, value) in dict(vars(self)).items()
175
+ if key not in ("current_loop_key", "app")
176
+ }
170
177
  loop_average = 0
171
178
  loop_minimum = None
172
179
  loop_maximum = None
@@ -332,7 +339,7 @@ def list_command(app: ApplicationContext, id_only: bool) -> None:
332
339
  Outputs a list of query URIs which can be used as reference for
333
340
  the query execute command.
334
341
  """
335
- queries = QUERY_CATALOG.get_queries().items()
342
+ queries = QueryCatalog().get_queries().items()
336
343
  if id_only:
337
344
  # sort dict by short_url - https://docs.python.org/3/howto/sorting.html
338
345
  for _, sparql_query in sorted(queries, key=lambda k: k[1].short_url.lower()):
@@ -444,7 +451,7 @@ def execute_command( # noqa: PLR0913
444
451
  app.echo_debug("Parameter: " + str(placeholder))
445
452
  for file_or_uri in queries:
446
453
  app.echo_debug(f"Start of execution: {file_or_uri} with " f"placeholder {placeholder}")
447
- executed_query = QUERY_CATALOG.get_query(file_or_uri, placeholder=placeholder)
454
+ executed_query = QueryCatalog().get_query(file_or_uri, placeholder=placeholder)
448
455
  if executed_query is None:
449
456
  raise ValueError(f"{file_or_uri} is neither a (readable) file nor a query URI.")
450
457
  app.echo_debug(
@@ -487,7 +494,7 @@ def open_command(app: ApplicationContext, queries: tuple[str, ...]) -> None:
487
494
  opening multiple browser tabs.
488
495
  """
489
496
  for file_or_uri in queries:
490
- opened_query = QUERY_CATALOG.get_query(file_or_uri)
497
+ opened_query = QueryCatalog().get_query(file_or_uri)
491
498
  if opened_query is None:
492
499
  raise ValueError(f"{file_or_uri} is neither a (readable) file nor a query URI.")
493
500
  open_query_uri = opened_query.get_editor_url()
@@ -576,7 +583,7 @@ def status_command(
576
583
  "REPLAY_FILE",
577
584
  required=True,
578
585
  shell_complete=completion.replay_files,
579
- type=click.Path(exists=True, allow_dash=False, readable=True, dir_okay=False),
586
+ type=ClickSmartPath(exists=True, allow_dash=False, readable=True, dir_okay=False),
580
587
  )
581
588
  @click.option("--raw", is_flag=True, help="Output the execution statistic as raw JSON.")
582
589
  @click.option(
@@ -604,7 +611,7 @@ def status_command(
604
611
  "of a successful command execution. The output can be stdout "
605
612
  "('-') - in this case, the execution statistic output is "
606
613
  "oppressed.",
607
- type=click.Path(exists=False, allow_dash=True, writable=True, dir_okay=False),
614
+ type=ClickSmartPath(exists=False, allow_dash=True, writable=True, dir_okay=False),
608
615
  )
609
616
  @click.option("--run-label", type=click.STRING, help="Optional label of this replay run.")
610
617
  @click.pass_obj
@@ -1,16 +1,21 @@
1
1
  """DataPlatform store commands for the cmem command line interface."""
2
2
 
3
3
  import os
4
- from pathlib import Path
4
+ from dataclasses import dataclass
5
5
 
6
6
  import click
7
7
  from click import UsageError
8
8
  from cmem.cmempy.dp.admin import create_showcase_data, delete_bootstrap_data, import_bootstrap_data
9
9
  from cmem.cmempy.dp.admin.backup import get_zip, post_zip
10
+ from cmem.cmempy.dp.workspace import migrate_workspaces
11
+ from cmem.cmempy.health import get_dp_info
10
12
 
11
13
  from cmem_cmemc import completion
12
14
  from cmem_cmemc.commands import CmemcCommand, CmemcGroup
13
15
  from cmem_cmemc.context import ApplicationContext
16
+ from cmem_cmemc.parameter_types.path import ClickSmartPath
17
+ from cmem_cmemc.smart_path import SmartPath as Path
18
+ from cmem_cmemc.utils import validate_zipfile
14
19
 
15
20
 
16
21
  @click.command(cls=CmemcCommand, name="bootstrap")
@@ -93,7 +98,7 @@ def showcase_command(app: ApplicationContext, scale: int, create: bool, delete:
93
98
  "BACKUP_FILE",
94
99
  shell_complete=completion.graph_backup_files,
95
100
  required=True,
96
- type=click.Path(writable=True, allow_dash=False, dir_okay=False),
101
+ type=ClickSmartPath(writable=True, allow_dash=False, dir_okay=False),
97
102
  )
98
103
  @click.option(
99
104
  "--overwrite",
@@ -133,7 +138,11 @@ def export_command(app: ApplicationContext, backup_file: str, overwrite: bool) -
133
138
  app.echo_info(".", nl=False)
134
139
  byte_counter = 0
135
140
  app.echo_debug(f"Wrote {overall_byte_counter} bytes to {backup_file}.")
136
- app.echo_success(" done")
141
+ if validate_zipfile(zipfile=backup_file):
142
+ app.echo_debug(f"{backup_file} successfully validated")
143
+ app.echo_success(" done")
144
+ else:
145
+ app.echo_error(" error (file corrupt)")
137
146
 
138
147
 
139
148
  @click.command(cls=CmemcCommand, name="import")
@@ -141,7 +150,7 @@ def export_command(app: ApplicationContext, backup_file: str, overwrite: bool) -
141
150
  "BACKUP_FILE",
142
151
  shell_complete=completion.graph_backup_files,
143
152
  required=True,
144
- type=click.Path(readable=True, exists=True, allow_dash=False, dir_okay=False),
153
+ type=ClickSmartPath(readable=True, exists=True, allow_dash=False, dir_okay=False),
145
154
  )
146
155
  @click.pass_obj
147
156
  def import_command(app: ApplicationContext, backup_file: str) -> None:
@@ -167,6 +176,66 @@ def import_command(app: ApplicationContext, backup_file: str) -> None:
167
176
  app.echo_success(" done")
168
177
 
169
178
 
179
+ @dataclass
180
+ class CommandResult:
181
+ """Represents the result of a command execution"""
182
+
183
+ data: list
184
+ headers: list[str]
185
+ caption: str
186
+ empty_state_message: str
187
+
188
+
189
+ def _migrate_workspaces() -> CommandResult:
190
+ """Migrate workspace configurations to the current CMEM version."""
191
+ request = migrate_workspaces()
192
+ return CommandResult(
193
+ data=[(iri,) for iri in request],
194
+ headers=["IRI"],
195
+ caption="Migrated workspace configurations",
196
+ empty_state_message="No migrateable workspace configurations found.",
197
+ )
198
+
199
+
200
+ def _get_migrate_workspaces() -> CommandResult:
201
+ """Retrieve workspace configurations that have been migrated to the current CMEM version."""
202
+ dp_info = get_dp_info()
203
+ migratable_workspace_configurations = dp_info["workspaceConfiguration"]["workspacesToMigrate"]
204
+ return CommandResult(
205
+ data=[(_["iri"], _["label"]) for _ in migratable_workspace_configurations],
206
+ headers=["IRI", "LABEL"],
207
+ caption="Migrateable configurations workspaces",
208
+ empty_state_message="No migrateable workspace configurations found.",
209
+ )
210
+
211
+
212
+ @click.command("migrate")
213
+ @click.option(
214
+ "--workspaces",
215
+ is_flag=True,
216
+ help="Migrate workspace configurations to the current version.",
217
+ )
218
+ @click.pass_obj
219
+ def migrate_command(app: ApplicationContext, workspaces: bool) -> None:
220
+ """Migrate configuration resources to the current version.
221
+
222
+ This command serves two purposes: (1) When invoked without an option, it lists
223
+ all migrateable configuration resources. (2) When invoked with the `--workspaces`
224
+ option, it migrates the workspace configurations to the current version.
225
+ """
226
+ result = _migrate_workspaces() if workspaces else _get_migrate_workspaces()
227
+
228
+ if result.data:
229
+ app.echo_info_table(
230
+ result.data,
231
+ headers=result.headers,
232
+ sort_column=0,
233
+ caption=result.caption,
234
+ )
235
+ else:
236
+ app.echo_success(result.empty_state_message)
237
+
238
+
170
239
  @click.group(cls=CmemcGroup)
171
240
  def store() -> CmemcGroup: # type: ignore[empty-body]
172
241
  """Import, export and bootstrap the knowledge graph store.
@@ -180,3 +249,4 @@ store.add_command(showcase_command)
180
249
  store.add_command(bootstrap_command)
181
250
  store.add_command(export_command)
182
251
  store.add_command(import_command)
252
+ store.add_command(migrate_command)
@@ -1,7 +1,11 @@
1
1
  """graph validation command group"""
2
2
 
3
+ import json
4
+ import sys
3
5
  import time
6
+ from collections import Counter
4
7
  from datetime import datetime, timezone
8
+ from pathlib import Path
5
9
 
6
10
  import click
7
11
  import requests
@@ -9,6 +13,7 @@ import timeago
9
13
  from click import Context, UsageError
10
14
  from click.shell_completion import CompletionItem
11
15
  from cmem.cmempy.dp.shacl import validation
16
+ from junit_xml import TestCase, TestSuite, to_xml_report_string
12
17
  from requests import HTTPError
13
18
  from rich.progress import Progress, SpinnerColumn, TaskID, TimeElapsedColumn
14
19
 
@@ -24,7 +29,62 @@ from cmem_cmemc.object_list import (
24
29
  compare_int_greater_than,
25
30
  transform_lower,
26
31
  )
27
- from cmem_cmemc.utils import struct_to_table
32
+ from cmem_cmemc.utils import get_query_text, struct_to_table
33
+
34
+
35
+ def _reports_to_junit(reports: list[dict]) -> str:
36
+ """Create a jUnit XML document from a list of report dictionaries"""
37
+ test_suites: list[TestSuite] = []
38
+
39
+ for report in reports:
40
+ test_cases: list[TestCase] = []
41
+ context_graph = report["contextGraphIri"]
42
+ shape_graph = report["shapeGraphIri"]
43
+ time_elapsed = (report["executionFinished"] - report["executionStarted"]) / 1000
44
+ violations: dict[str, list[dict]] = {}
45
+ for resource in sorted(report["resources"]):
46
+ # get a list of all tested resources
47
+ violations[resource] = []
48
+ average_elapsed_sec = time_elapsed / len(violations)
49
+ for result in report["results"]:
50
+ # collection violations per resource
51
+ resource_iri = result["resourceIri"]
52
+ violations[resource_iri] = result["violations"]
53
+ for resource in violations:
54
+ # create on test case per resource
55
+ resource_violations = violations[resource]
56
+ violations_count = len(violations[resource])
57
+ constraints = Counter(
58
+ _["reportEntryConstraintMessageTemplate"]["constraintName"]
59
+ for _ in resource_violations
60
+ )
61
+ test_case_name = f"{resource}"
62
+ if violations_count == 0:
63
+ test_case_name += " has no violations"
64
+ if violations_count == 1:
65
+ test_case_name += f" has 1 violation ({next(iter(constraints.keys()))})"
66
+ if violations_count > 1:
67
+ constrains_str = ""
68
+ for constraint, constraint_count in constraints.items():
69
+ constrains_str += f", {constraint_count}x{constraint}"
70
+ test_case_name += f" has {violations_count} violations ({constrains_str[2:]})"
71
+
72
+ test_case = TestCase(
73
+ name=test_case_name,
74
+ classname=f"{context_graph} tested with {shape_graph}",
75
+ elapsed_sec=average_elapsed_sec,
76
+ )
77
+ if violations_count > 0:
78
+ test_case.add_failure_info(output=json.dumps(resource_violations, indent=2))
79
+ test_cases.append(test_case)
80
+ test_suite = TestSuite(
81
+ name=f"Testing {context_graph} with shapes from {shape_graph}.",
82
+ test_cases=test_cases,
83
+ id=report["id"],
84
+ timestamp=report["executionFinished"],
85
+ )
86
+ test_suites.append(test_suite)
87
+ return str(to_xml_report_string(test_suites, encoding="utf-8"))
28
88
 
29
89
 
30
90
  def get_sorted_validations_list(ctx: Context) -> list[dict]: # noqa: ARG001
@@ -179,6 +239,20 @@ def _complete_running_batch_validations(
179
239
  return _finalize_completion(candidates=options, incomplete=incomplete)
180
240
 
181
241
 
242
+ def _complete_finished_batch_validations(
243
+ ctx: click.Context, # noqa: ARG001
244
+ param: click.Argument, # noqa: ARG001
245
+ incomplete: str,
246
+ ) -> list[CompletionItem]:
247
+ """Provide completion for finished batch validations"""
248
+ options = [
249
+ _get_batch_validation_option(_)
250
+ for _ in validation.get_all_aggregations()
251
+ if _["state"] == validation.STATUS_FINISHED
252
+ ]
253
+ return _finalize_completion(candidates=options, incomplete=incomplete)
254
+
255
+
182
256
  def show_process_summary(app: ApplicationContext, process_id: str) -> None:
183
257
  """Show summary of the validation process"""
184
258
  app.echo_info_table(
@@ -307,6 +381,12 @@ def _get_violation_count(process_data: dict) -> str:
307
381
 
308
382
  @click.command(cls=CmemcCommand, name="execute")
309
383
  @click.argument("iri", type=click.STRING, shell_complete=completion.graph_uris)
384
+ @click.option(
385
+ "--wait",
386
+ is_flag=True,
387
+ help="Wait until the process is finished. When using this option without the "
388
+ "`--id-only` flag, it will enable a progress bar and a summary view.",
389
+ )
310
390
  @click.option(
311
391
  "--shape-graph",
312
392
  shell_complete=completion.graph_uris,
@@ -315,16 +395,39 @@ def _get_violation_count(process_data: dict) -> str:
315
395
  help="The shape catalog used for validation.",
316
396
  )
317
397
  @click.option(
318
- "--id-only",
398
+ "--query",
399
+ shell_complete=completion.remote_queries_and_sparql_files,
400
+ help="SPARQL query to select the resources which you want to validate from "
401
+ "the data graph. "
402
+ "Can be provided as a local file or as a query catalog IRI. "
403
+ "[default: all typed resources]",
404
+ )
405
+ @click.option(
406
+ "--result-graph",
407
+ shell_complete=completion.writable_graph_uris,
408
+ help="(Optionally) write the validation results to a Knowledge Graph. " "[default: None]",
409
+ )
410
+ @click.option(
411
+ "--replace",
319
412
  is_flag=True,
320
- help="Return the validation process identifier only. "
321
- "This is useful for piping the ID into other commands.",
413
+ default=False,
414
+ help="Replace the result graph instead of just adding the new results. "
415
+ "This is a dangerous option, so use it with care!",
322
416
  )
323
417
  @click.option(
324
- "--wait",
418
+ "--ignore-graph",
419
+ shell_complete=completion.ignore_graph_uris,
420
+ type=click.STRING,
421
+ multiple=True,
422
+ help="A set of data graph IRIs which are not queried in the resource selection. "
423
+ "This option is useful for validating only parts of an integration graph "
424
+ "which imports other graphs.",
425
+ )
426
+ @click.option(
427
+ "--id-only",
325
428
  is_flag=True,
326
- help="Wait until the process is finished. When using this option without the "
327
- "`--id-only` flag, it will enable a progress bar and a summary view.",
429
+ help="Return the validation process identifier only. "
430
+ "This is useful for piping the ID into other commands.",
328
431
  )
329
432
  @click.option(
330
433
  "--polling-interval",
@@ -339,17 +442,31 @@ def execute_command( # noqa: PLR0913
339
442
  app: ApplicationContext,
340
443
  iri: str,
341
444
  shape_graph: str,
445
+ query: str,
446
+ result_graph: str,
447
+ replace: bool,
448
+ ignore_graph: list[str],
342
449
  id_only: bool,
343
450
  wait: bool,
344
451
  polling_interval: int,
345
452
  ) -> None:
346
453
  """Start a new validation process.
347
454
 
348
- Validation is performed on all typed resources of a data / context graph (IRI).
349
- Each resource is validated against all applicable node shapes from a
350
- selected shape catalog graph (and its sub-graphs).
455
+ Validation is performed on all typed resources of the data / context graph
456
+ (and its sub-graphs). Each resource is validated against all applicable node
457
+ shapes from the shape catalog.
351
458
  """
352
- process_id = validation.start(context_graph=iri, shape_graph=shape_graph)
459
+ query_str = None
460
+ if query:
461
+ query_str = get_query_text(query, {"resource"})
462
+ process_id = validation.start(
463
+ context_graph=iri,
464
+ shape_graph=shape_graph,
465
+ query=query_str,
466
+ result_graph=result_graph,
467
+ replace=replace,
468
+ ignore_graph=ignore_graph,
469
+ )
353
470
  if wait:
354
471
  _wait_for_process_completion(
355
472
  app=app, process_id=process_id, use_rich=not id_only, polling_interval=polling_interval
@@ -532,6 +649,94 @@ def cancel_command(app: ApplicationContext, process_id: str) -> None:
532
649
  app.echo_success("cancelled")
533
650
 
534
651
 
652
+ @click.command(cls=CmemcCommand, name="export")
653
+ @click.argument(
654
+ "process_ids", nargs=-1, type=click.STRING, shell_complete=_complete_finished_batch_validations
655
+ )
656
+ @click.option(
657
+ "--output-file",
658
+ type=click.Path(writable=True, allow_dash=False, dir_okay=False),
659
+ default="report.xml",
660
+ show_default=True,
661
+ help="Export the report to this file. Existing files will be overwritten.",
662
+ )
663
+ @click.option(
664
+ "--exit-1",
665
+ type=click.Choice(["never", "error"]),
666
+ default="error",
667
+ show_default=True,
668
+ help="Specify, when this command returns with exit code 1. Available options are "
669
+ "'never' (exit 0, even if there are violations in the reports), "
670
+ "'error' (exit 1 if there is at least one violation in a report).), ",
671
+ )
672
+ @click.option(
673
+ "--format",
674
+ "format_",
675
+ type=click.Choice(["JSON", "XML"], case_sensitive=True),
676
+ default="XML",
677
+ help="Export either the plain JSON report or a distilled jUnit XML report.",
678
+ show_default=True,
679
+ )
680
+ @click.pass_context
681
+ def export_command(
682
+ ctx: Context, process_ids: tuple[str], output_file: str, exit_1: str, format_: str
683
+ ) -> None:
684
+ """Export a report of finished validations.
685
+
686
+ This command exports a jUnit XML or JSON report in order to process
687
+ them somewhere else (e.g. a CI pipeline).
688
+
689
+ You can export a single report of multiple validation processes.
690
+
691
+ For jUnit XML: Each validation process result will be transformed to
692
+ a single test suite. All violations of one resource in a result will be
693
+ collected and attached to a single test case in that test suite.
694
+
695
+ Note: Validation processes IDs can be listed with the `graph validation list`
696
+ command, or by utilizing the tab completion of this command.
697
+ """
698
+ if len(process_ids) == 0:
699
+ raise UsageError("This command needs at least one validation process ID.")
700
+ app: ApplicationContext = ctx.obj
701
+ process_ids_to_test = {_: True for _ in process_ids}
702
+ overall_violations = 0
703
+ overall_resources = 0
704
+ for _ in validation.get_all_aggregations():
705
+ if _["id"] in process_ids_to_test:
706
+ if _["state"] != "FINISHED":
707
+ raise UsageError(f"Validation process with ID '{_['id']}' is still running.")
708
+ del process_ids_to_test[_["id"]]
709
+ overall_violations += int(_["violationsCount"])
710
+ overall_resources += int(_["resourceProcessedCount"])
711
+ if len(process_ids_to_test) > 0:
712
+ raise UsageError(
713
+ "Validation processes with the following IDs not known (anymore): "
714
+ + ", ".join(process_ids_to_test)
715
+ )
716
+ reports = []
717
+ for process_id in process_ids:
718
+ report = validation.get(batch_id=process_id)
719
+ reports.append(report)
720
+ app.echo_info(
721
+ f"Export of {len(reports)} validation report(s) with"
722
+ f" {overall_violations} violations in {overall_resources} resources"
723
+ f" to {output_file} ... ",
724
+ nl=False,
725
+ )
726
+ with Path(output_file, mode="w", encoding="utf-8") as file:
727
+ if format_ == "XML":
728
+ file.write_text(_reports_to_junit(reports))
729
+ if format_ == "JSON":
730
+ file.write_text(json.dumps(reports, indent=2))
731
+ app.echo_success("done")
732
+ if exit_1 == "error" and overall_violations > 0:
733
+ app.echo_error(
734
+ "Exit 1 since violations where found in the reports "
735
+ "(can be suppressed with '--exit-1 never')."
736
+ )
737
+ sys.exit(1)
738
+
739
+
535
740
  @click.group(cls=CmemcGroup, name="validation")
536
741
  def validation_group() -> CmemcGroup: # type: ignore[empty-body]
537
742
  """Validate resources in a graph.
@@ -552,3 +757,4 @@ validation_group.add_command(execute_command)
552
757
  validation_group.add_command(list_command)
553
758
  validation_group.add_command(inspect_command)
554
759
  validation_group.add_command(cancel_command)
760
+ validation_group.add_command(export_command)
@@ -24,6 +24,7 @@ from six.moves.urllib.parse import quote
24
24
  from cmem_cmemc import completion
25
25
  from cmem_cmemc.commands import CmemcCommand, CmemcGroup
26
26
  from cmem_cmemc.context import ApplicationContext
27
+ from cmem_cmemc.parameter_types.path import ClickSmartPath
27
28
 
28
29
  GET_ONTOLOGY_IRI_QUERY = """
29
30
  PREFIX owl: <http://www.w3.org/2002/07/owl#>
@@ -72,22 +73,29 @@ def _validate_vocabs_to_process(iris: tuple[str], filter_: str, all_flag: bool)
72
73
  list is without duplicates, and validated if they exist
73
74
  """
74
75
  if iris == () and not all_flag:
75
- raise ValueError(
76
+ raise click.UsageError(
76
77
  "Either specify at least one vocabulary IRI "
77
78
  "or use the --all option to process over all vocabularies."
78
79
  )
79
- vocabs_to_process = list(iris)
80
- all_vocabs = [_["iri"] for _ in get_vocabularies(filter_=filter_)]
80
+ all_vocabs = {_["iri"]: _ for _ in get_vocabularies()}
81
81
  if all_flag:
82
- # in case --all is given, all vocabs are processed
83
- vocabs_to_process = all_vocabs
84
- # avoid double removal
85
- vocabs_to_process = list(set(vocabs_to_process))
86
-
87
- # test if one of the vocabs does not exist
82
+ # in case --all is given, all installable / installed vocabs are processed
83
+ if filter_ == "installed": # uninstall command
84
+ return [_ for _ in all_vocabs if all_vocabs[_]["installed"]]
85
+ # install command
86
+ return [_ for _ in all_vocabs if not all_vocabs[_]["installed"]]
87
+
88
+ vocabs_to_process = list(set(iris)) # avoid double removal / installation
89
+ # test if one of the vocabs does not exist or is already installed / not installed
88
90
  for _ in vocabs_to_process:
89
- if _ not in all_vocabs:
90
- raise ValueError(f"Vocabulary {_} does not exist.")
91
+ # uninstall command
92
+ if filter_ == "installed" and (_ not in all_vocabs or not all_vocabs[_]["installed"]):
93
+ raise click.UsageError(f"Vocabulary {_} not installed.")
94
+ if filter_ == "installable": # install command
95
+ if _ not in all_vocabs:
96
+ raise click.UsageError(f"Vocabulary {_} does not exist.")
97
+ if all_vocabs[_]["installed"]:
98
+ raise click.UsageError(f"Vocabulary {_} already installed.")
91
99
  return vocabs_to_process
92
100
 
93
101
 
@@ -316,10 +324,7 @@ def uninstall_command(app: ApplicationContext, iris: tuple[str], all_: bool) ->
316
324
  "FILE",
317
325
  required=True,
318
326
  shell_complete=completion.triple_files,
319
- type=click.Path(
320
- allow_dash=True,
321
- readable=True,
322
- ),
327
+ type=ClickSmartPath(allow_dash=True, readable=True, remote_okay=True),
323
328
  )
324
329
  @click.option(
325
330
  "--namespace",
@@ -347,7 +352,7 @@ def import_command(
347
352
  `vann:preferredNamespaceUri` properties.
348
353
  """
349
354
  _buffer = io.BytesIO()
350
- with click.open_file(file, "rb") as file_handle:
355
+ with ClickSmartPath.open(file) as file_handle:
351
356
  _buffer.write(file_handle.read())
352
357
  _buffer.seek(0)
353
358