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.
- cmem_cmemc/__init__.py +2 -2
- cmem_cmemc/commands/__init__.py +1 -0
- cmem_cmemc/commands/acl.py +101 -51
- cmem_cmemc/commands/admin.py +18 -6
- cmem_cmemc/commands/dataset.py +11 -9
- cmem_cmemc/commands/graph.py +17 -5
- cmem_cmemc/commands/project.py +19 -7
- cmem_cmemc/commands/python.py +31 -21
- cmem_cmemc/commands/query.py +20 -13
- cmem_cmemc/commands/store.py +74 -4
- cmem_cmemc/commands/validation.py +217 -11
- cmem_cmemc/commands/vocabulary.py +21 -16
- cmem_cmemc/commands/workflow.py +71 -52
- cmem_cmemc/commands/workspace.py +4 -3
- cmem_cmemc/completion.py +35 -16
- cmem_cmemc/context.py +35 -13
- cmem_cmemc/object_list.py +4 -12
- cmem_cmemc/parameter_types/__init__.py +1 -0
- cmem_cmemc/parameter_types/path.py +63 -0
- cmem_cmemc/smart_path/__init__.py +94 -0
- cmem_cmemc/smart_path/clients/__init__.py +63 -0
- cmem_cmemc/smart_path/clients/http.py +65 -0
- cmem_cmemc/utils.py +53 -3
- {cmem_cmemc-24.1.5.dist-info → cmem_cmemc-24.2.0.dist-info}/METADATA +19 -16
- cmem_cmemc-24.2.0.dist-info/RECORD +42 -0
- cmem_cmemc-24.1.5.dist-info/RECORD +0 -37
- {cmem_cmemc-24.1.5.dist-info → cmem_cmemc-24.2.0.dist-info}/LICENSE +0 -0
- {cmem_cmemc-24.1.5.dist-info → cmem_cmemc-24.2.0.dist-info}/WHEEL +0 -0
- {cmem_cmemc-24.1.5.dist-info → cmem_cmemc-24.2.0.dist-info}/entry_points.txt +0 -0
cmem_cmemc/commands/query.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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=
|
|
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=
|
|
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
|
cmem_cmemc/commands/store.py
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"""DataPlatform store commands for the cmem command line interface."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
|
-
"--
|
|
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
|
-
|
|
321
|
-
"
|
|
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
|
-
"--
|
|
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="
|
|
327
|
-
"
|
|
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
|
|
349
|
-
Each resource is validated against all applicable node
|
|
350
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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=
|
|
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
|
|
355
|
+
with ClickSmartPath.open(file) as file_handle:
|
|
351
356
|
_buffer.write(file_handle.read())
|
|
352
357
|
_buffer.seek(0)
|
|
353
358
|
|