cmem-cmemc 24.2.0rc1__py3-none-any.whl → 24.3.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.
- cmem_cmemc/__init__.py +7 -12
- cmem_cmemc/command.py +20 -0
- cmem_cmemc/command_group.py +70 -0
- cmem_cmemc/commands/__init__.py +0 -81
- cmem_cmemc/commands/acl.py +118 -62
- cmem_cmemc/commands/admin.py +46 -35
- cmem_cmemc/commands/client.py +2 -1
- cmem_cmemc/commands/config.py +3 -1
- cmem_cmemc/commands/dataset.py +27 -24
- cmem_cmemc/commands/graph.py +100 -16
- cmem_cmemc/commands/metrics.py +195 -79
- cmem_cmemc/commands/migration.py +265 -0
- cmem_cmemc/commands/project.py +62 -17
- cmem_cmemc/commands/python.py +57 -26
- cmem_cmemc/commands/query.py +23 -14
- cmem_cmemc/commands/resource.py +10 -2
- cmem_cmemc/commands/scheduler.py +10 -2
- cmem_cmemc/commands/store.py +118 -14
- cmem_cmemc/commands/user.py +8 -2
- cmem_cmemc/commands/validation.py +304 -113
- cmem_cmemc/commands/variable.py +10 -2
- cmem_cmemc/commands/vocabulary.py +48 -29
- cmem_cmemc/commands/workflow.py +86 -59
- cmem_cmemc/commands/workspace.py +27 -8
- cmem_cmemc/completion.py +185 -141
- cmem_cmemc/constants.py +2 -0
- cmem_cmemc/context.py +88 -42
- cmem_cmemc/manual_helper/graph.py +1 -0
- cmem_cmemc/manual_helper/multi_page.py +3 -1
- cmem_cmemc/migrations/__init__.py +1 -0
- cmem_cmemc/migrations/abc.py +84 -0
- cmem_cmemc/migrations/access_conditions_243.py +118 -0
- cmem_cmemc/migrations/bootstrap_data.py +30 -0
- cmem_cmemc/migrations/shapes_widget_integrations_243.py +194 -0
- cmem_cmemc/migrations/workspace_configurations.py +28 -0
- cmem_cmemc/object_list.py +53 -22
- cmem_cmemc/parameter_types/__init__.py +1 -0
- cmem_cmemc/parameter_types/path.py +69 -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/string_processor.py +77 -0
- cmem_cmemc/title_helper.py +41 -0
- cmem_cmemc/utils.py +114 -47
- {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0rc1.dist-info}/LICENSE +1 -1
- cmem_cmemc-24.3.0rc1.dist-info/METADATA +89 -0
- cmem_cmemc-24.3.0rc1.dist-info/RECORD +53 -0
- {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0rc1.dist-info}/WHEEL +1 -1
- cmem_cmemc-24.2.0rc1.dist-info/METADATA +0 -69
- cmem_cmemc-24.2.0rc1.dist-info/RECORD +0 -37
- {cmem_cmemc-24.2.0rc1.dist-info → cmem_cmemc-24.3.0rc1.dist-info}/entry_points.txt +0 -0
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
"""graph validation command group"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
2
5
|
import time
|
|
3
|
-
from
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
4
9
|
|
|
5
10
|
import click
|
|
6
|
-
import requests
|
|
7
11
|
import timeago
|
|
8
12
|
from click import Context, UsageError
|
|
9
13
|
from click.shell_completion import CompletionItem
|
|
14
|
+
from cmem.cmempy.config import get_cmem_base_uri
|
|
10
15
|
from cmem.cmempy.dp.shacl import validation
|
|
11
16
|
from junit_xml import TestCase, TestSuite, to_xml_report_string
|
|
12
|
-
from requests import HTTPError
|
|
13
17
|
from rich.progress import Progress, SpinnerColumn, TaskID, TimeElapsedColumn
|
|
14
18
|
|
|
15
19
|
from cmem_cmemc import completion
|
|
16
|
-
from cmem_cmemc.
|
|
17
|
-
from cmem_cmemc.
|
|
20
|
+
from cmem_cmemc.command import CmemcCommand
|
|
21
|
+
from cmem_cmemc.command_group import CmemcGroup
|
|
22
|
+
from cmem_cmemc.completion import finalize_completion
|
|
18
23
|
from cmem_cmemc.context import ApplicationContext
|
|
19
24
|
from cmem_cmemc.exceptions import ServerError
|
|
20
25
|
from cmem_cmemc.object_list import (
|
|
@@ -24,27 +29,64 @@ from cmem_cmemc.object_list import (
|
|
|
24
29
|
compare_int_greater_than,
|
|
25
30
|
transform_lower,
|
|
26
31
|
)
|
|
27
|
-
from cmem_cmemc.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
from cmem_cmemc.string_processor import GraphLink, ResourceLink, TimeAgo
|
|
33
|
+
from cmem_cmemc.title_helper import TitleHelper
|
|
34
|
+
from cmem_cmemc.utils import get_query_text, struct_to_table
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _reports_to_junit(reports: list[dict]) -> str:
|
|
38
|
+
"""Create a jUnit XML document from a list of report dictionaries"""
|
|
39
|
+
test_suites: list[TestSuite] = []
|
|
40
|
+
|
|
41
|
+
for report in reports:
|
|
42
|
+
test_cases: list[TestCase] = []
|
|
43
|
+
context_graph = report["contextGraphIri"]
|
|
44
|
+
shape_graph = report["shapeGraphIri"]
|
|
45
|
+
time_elapsed = (report["executionFinished"] - report["executionStarted"]) / 1000
|
|
46
|
+
violations: dict[str, list[dict]] = {}
|
|
47
|
+
for resource in sorted(report["resources"]):
|
|
48
|
+
# get a list of all tested resources
|
|
49
|
+
violations[resource] = []
|
|
50
|
+
average_elapsed_sec = time_elapsed / len(violations)
|
|
51
|
+
for result in report["results"]:
|
|
52
|
+
# collection violations per resource
|
|
53
|
+
resource_iri = result["resourceIri"]
|
|
54
|
+
violations[resource_iri] = result["violations"]
|
|
55
|
+
for resource in violations:
|
|
56
|
+
# create on test case per resource
|
|
57
|
+
resource_violations = violations[resource]
|
|
58
|
+
violations_count = len(violations[resource])
|
|
59
|
+
constraints = Counter(
|
|
60
|
+
_["reportEntryConstraintMessageTemplate"]["constraintName"]
|
|
61
|
+
for _ in resource_violations
|
|
44
62
|
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
test_case_name = f"{resource}"
|
|
64
|
+
if violations_count == 0:
|
|
65
|
+
test_case_name += " has no violations"
|
|
66
|
+
if violations_count == 1:
|
|
67
|
+
test_case_name += f" has 1 violation ({next(iter(constraints.keys()))})"
|
|
68
|
+
if violations_count > 1:
|
|
69
|
+
constrains_str = ""
|
|
70
|
+
for constraint, constraint_count in constraints.items():
|
|
71
|
+
constrains_str += f", {constraint_count}x{constraint}"
|
|
72
|
+
test_case_name += f" has {violations_count} violations ({constrains_str[2:]})"
|
|
73
|
+
|
|
74
|
+
test_case = TestCase(
|
|
75
|
+
name=test_case_name,
|
|
76
|
+
classname=f"{context_graph} tested with {shape_graph}",
|
|
77
|
+
elapsed_sec=average_elapsed_sec,
|
|
78
|
+
)
|
|
79
|
+
if violations_count > 0:
|
|
80
|
+
test_case.add_failure_info(output=json.dumps(resource_violations, indent=2))
|
|
81
|
+
test_cases.append(test_case)
|
|
82
|
+
test_suite = TestSuite(
|
|
83
|
+
name=f"Testing {context_graph} with shapes from {shape_graph}.",
|
|
84
|
+
test_cases=test_cases,
|
|
85
|
+
id=report["id"],
|
|
86
|
+
timestamp=report["executionFinished"],
|
|
87
|
+
)
|
|
88
|
+
test_suites.append(test_suite)
|
|
89
|
+
return str(to_xml_report_string(test_suites, encoding="utf-8"))
|
|
48
90
|
|
|
49
91
|
|
|
50
92
|
def get_sorted_validations_list(ctx: Context) -> list[dict]: # noqa: ARG001
|
|
@@ -144,17 +186,22 @@ violations_list = ObjectList(
|
|
|
144
186
|
name="severity", description="Filter list by severity.", property_key="severity"
|
|
145
187
|
),
|
|
146
188
|
DirectValuePropertyFilter(
|
|
147
|
-
name="resource",
|
|
189
|
+
name="resource",
|
|
190
|
+
description="Filter list by resource IRI.",
|
|
191
|
+
property_key="resourceIri",
|
|
192
|
+
title_helper=TitleHelper(),
|
|
148
193
|
),
|
|
149
194
|
DirectListPropertyFilter(
|
|
150
195
|
name="node-shape",
|
|
151
196
|
description="Filter list by node shape IRI.",
|
|
152
197
|
property_key="nodeShapes",
|
|
198
|
+
title_helper=TitleHelper(),
|
|
153
199
|
),
|
|
154
200
|
DirectValuePropertyFilter(
|
|
155
|
-
name="
|
|
156
|
-
description="Filter list by
|
|
201
|
+
name="source",
|
|
202
|
+
description="Filter list by constraint source.",
|
|
157
203
|
property_key="source",
|
|
204
|
+
title_helper=TitleHelper(),
|
|
158
205
|
),
|
|
159
206
|
],
|
|
160
207
|
)
|
|
@@ -165,8 +212,8 @@ def _get_batch_validation_option(validation_: dict) -> tuple[str, str]:
|
|
|
165
212
|
id_ = validation_["id"]
|
|
166
213
|
state = validation_["state"]
|
|
167
214
|
graph = validation_["contextGraphIri"]
|
|
168
|
-
stamp = datetime.fromtimestamp(validation_["executionStarted"] / 1000, tz=
|
|
169
|
-
time_ago = timeago.format(stamp, datetime.now(tz=
|
|
215
|
+
stamp = datetime.fromtimestamp(validation_["executionStarted"] / 1000, tz=timezone.utc)
|
|
216
|
+
time_ago = timeago.format(stamp, datetime.now(tz=timezone.utc))
|
|
170
217
|
resources = _get_resource_count(validation_)
|
|
171
218
|
violations = _get_violation_count(validation_)
|
|
172
219
|
return (
|
|
@@ -182,7 +229,7 @@ def _complete_all_batch_validations(
|
|
|
182
229
|
) -> list[CompletionItem]:
|
|
183
230
|
"""Provide completion for batch validation"""
|
|
184
231
|
options = [_get_batch_validation_option(_) for _ in validation.get_all_aggregations()]
|
|
185
|
-
return
|
|
232
|
+
return finalize_completion(candidates=options, incomplete=incomplete)
|
|
186
233
|
|
|
187
234
|
|
|
188
235
|
def _complete_running_batch_validations(
|
|
@@ -196,10 +243,24 @@ def _complete_running_batch_validations(
|
|
|
196
243
|
for _ in validation.get_all_aggregations()
|
|
197
244
|
if _["state"] == validation.STATUS_RUNNING
|
|
198
245
|
]
|
|
199
|
-
return
|
|
246
|
+
return finalize_completion(candidates=options, incomplete=incomplete)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _complete_finished_batch_validations(
|
|
250
|
+
ctx: click.Context, # noqa: ARG001
|
|
251
|
+
param: click.Argument, # noqa: ARG001
|
|
252
|
+
incomplete: str,
|
|
253
|
+
) -> list[CompletionItem]:
|
|
254
|
+
"""Provide completion for finished batch validations"""
|
|
255
|
+
options = [
|
|
256
|
+
_get_batch_validation_option(_)
|
|
257
|
+
for _ in validation.get_all_aggregations()
|
|
258
|
+
if _["state"] == validation.STATUS_FINISHED
|
|
259
|
+
]
|
|
260
|
+
return finalize_completion(candidates=options, incomplete=incomplete)
|
|
200
261
|
|
|
201
262
|
|
|
202
|
-
def
|
|
263
|
+
def _print_process_summary(app: ApplicationContext, process_id: str) -> None:
|
|
203
264
|
"""Show summary of the validation process"""
|
|
204
265
|
app.echo_info_table(
|
|
205
266
|
struct_to_table(validation.get_aggregation(batch_id=process_id)),
|
|
@@ -276,33 +337,64 @@ def _wait_for_process_completion(
|
|
|
276
337
|
return state.status
|
|
277
338
|
|
|
278
339
|
|
|
279
|
-
def
|
|
280
|
-
|
|
340
|
+
def _print_violation_table(
|
|
341
|
+
app: ApplicationContext, data_graph: str, shape_graph: str, violations: list[dict]
|
|
342
|
+
) -> None:
|
|
343
|
+
"""Print violation table from batch validation result"""
|
|
344
|
+
# fetch titles
|
|
345
|
+
resources = []
|
|
346
|
+
for violation in violations:
|
|
347
|
+
resources.append(str(violation.get("resourceIri")))
|
|
348
|
+
resources.extend(violation.get("nodeShapes", []))
|
|
349
|
+
title_helper = TitleHelper()
|
|
350
|
+
title_helper.get(resources)
|
|
351
|
+
|
|
352
|
+
# prepare link helper
|
|
353
|
+
resource_link = ResourceLink(graph_iri=data_graph, title_helper=title_helper)
|
|
354
|
+
shape_link = ResourceLink(graph_iri=shape_graph, title_helper=title_helper)
|
|
355
|
+
|
|
281
356
|
table = []
|
|
282
357
|
for violation in violations:
|
|
283
|
-
|
|
358
|
+
combined_cell = ""
|
|
359
|
+
|
|
284
360
|
path = violation.get("path", None)
|
|
285
|
-
|
|
361
|
+
if path is not None:
|
|
362
|
+
combined_cell = f"Path: {path}\n"
|
|
363
|
+
|
|
364
|
+
source = violation.get("source", None)
|
|
365
|
+
if source is not None:
|
|
366
|
+
combined_cell = f"{combined_cell}Source: {shape_link.process(text=source)}"
|
|
367
|
+
|
|
286
368
|
node_shapes = violation.get("nodeShapes", [])
|
|
369
|
+
if len(node_shapes) == 1:
|
|
370
|
+
combined_cell = f"{combined_cell}\nNodeShape: {shape_link.process(text=node_shapes[0])}"
|
|
371
|
+
if len(node_shapes) > 1:
|
|
372
|
+
combined_cell = f"{combined_cell}\nNodeShapes:"
|
|
373
|
+
for node_shape in node_shapes:
|
|
374
|
+
combined_cell = f"{combined_cell}\n - {shape_link.process(text=node_shape)}"
|
|
375
|
+
|
|
287
376
|
text = violation["messages"][0]["value"] # default: use the text of the first message
|
|
288
377
|
for message in violation["messages"]:
|
|
289
378
|
# look for en non non-lang messages to use
|
|
290
379
|
if message["lang"] == "" or message["lang"] == "en":
|
|
291
380
|
text = str(message["value"])
|
|
292
381
|
break
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
for node_shape in node_shapes:
|
|
301
|
-
cell = f"{cell}\n - {node_shape}"
|
|
302
|
-
cell = f"{cell}\nMessage: {text}"
|
|
303
|
-
row = [resource_iri, constraint_name, cell]
|
|
382
|
+
combined_cell = f"{combined_cell}\nMessage: {text}"
|
|
383
|
+
|
|
384
|
+
row = [
|
|
385
|
+
resource_link.process(text=str(violation.get("resourceIri"))),
|
|
386
|
+
violation.get("constraintName", "UNKNOWN"),
|
|
387
|
+
combined_cell,
|
|
388
|
+
]
|
|
304
389
|
table.append(row)
|
|
305
|
-
|
|
390
|
+
|
|
391
|
+
app.echo_info_table(
|
|
392
|
+
table,
|
|
393
|
+
headers=["Resource", "Constraint", "Details"],
|
|
394
|
+
sort_column=0,
|
|
395
|
+
caption="Violation List",
|
|
396
|
+
empty_table_message="No violations found.",
|
|
397
|
+
)
|
|
306
398
|
|
|
307
399
|
|
|
308
400
|
def _get_resource_count(batch_validation: dict) -> str:
|
|
@@ -316,7 +408,7 @@ def _get_resource_count(batch_validation: dict) -> str:
|
|
|
316
408
|
|
|
317
409
|
def _get_violation_count(process_data: dict) -> str:
|
|
318
410
|
"""Get violation count from validation report"""
|
|
319
|
-
if process_data.get("executionStarted"
|
|
411
|
+
if process_data.get("executionStarted") is None:
|
|
320
412
|
return "-"
|
|
321
413
|
resources = str(process_data.get("resourcesWithViolationsCount", "0"))
|
|
322
414
|
violations = str(process_data.get("violationsCount", "0"))
|
|
@@ -327,13 +419,48 @@ def _get_violation_count(process_data: dict) -> str:
|
|
|
327
419
|
|
|
328
420
|
@click.command(cls=CmemcCommand, name="execute")
|
|
329
421
|
@click.argument("iri", type=click.STRING, shell_complete=completion.graph_uris)
|
|
422
|
+
@click.option(
|
|
423
|
+
"--wait",
|
|
424
|
+
is_flag=True,
|
|
425
|
+
help="Wait until the process is finished. When using this option without the "
|
|
426
|
+
"`--id-only` flag, it will enable a progress bar and a summary view.",
|
|
427
|
+
)
|
|
330
428
|
@click.option(
|
|
331
429
|
"--shape-graph",
|
|
332
|
-
shell_complete=completion.
|
|
430
|
+
shell_complete=completion.graph_uris_skip_check,
|
|
333
431
|
default="https://vocab.eccenca.com/shacl/",
|
|
334
432
|
show_default=True,
|
|
335
433
|
help="The shape catalog used for validation.",
|
|
336
434
|
)
|
|
435
|
+
@click.option(
|
|
436
|
+
"--query",
|
|
437
|
+
shell_complete=completion.remote_queries_and_sparql_files,
|
|
438
|
+
help="SPARQL query to select the resources which you want to validate from "
|
|
439
|
+
"the data graph. "
|
|
440
|
+
"Can be provided as a local file or as a query catalog IRI. "
|
|
441
|
+
"[default: all typed resources]",
|
|
442
|
+
)
|
|
443
|
+
@click.option(
|
|
444
|
+
"--result-graph",
|
|
445
|
+
shell_complete=completion.writable_graph_uris,
|
|
446
|
+
help="(Optionally) write the validation results to a Knowledge Graph. " "[default: None]",
|
|
447
|
+
)
|
|
448
|
+
@click.option(
|
|
449
|
+
"--replace",
|
|
450
|
+
is_flag=True,
|
|
451
|
+
default=False,
|
|
452
|
+
help="Replace the result graph instead of just adding the new results. "
|
|
453
|
+
"This is a dangerous option, so use it with care!",
|
|
454
|
+
)
|
|
455
|
+
@click.option(
|
|
456
|
+
"--ignore-graph",
|
|
457
|
+
shell_complete=completion.ignore_graph_uris,
|
|
458
|
+
type=click.STRING,
|
|
459
|
+
multiple=True,
|
|
460
|
+
help="A set of data graph IRIs which are not queried in the resource selection. "
|
|
461
|
+
"This option is useful for validating only parts of an integration graph "
|
|
462
|
+
"which imports other graphs.",
|
|
463
|
+
)
|
|
337
464
|
@click.option(
|
|
338
465
|
"--id-only",
|
|
339
466
|
is_flag=True,
|
|
@@ -341,10 +468,9 @@ def _get_violation_count(process_data: dict) -> str:
|
|
|
341
468
|
"This is useful for piping the ID into other commands.",
|
|
342
469
|
)
|
|
343
470
|
@click.option(
|
|
344
|
-
"--
|
|
471
|
+
"--inspect",
|
|
345
472
|
is_flag=True,
|
|
346
|
-
help="
|
|
347
|
-
"`--id-only` flag, it will enable a progress bar and a summary view.",
|
|
473
|
+
help="Return the list of violations instead of the summary (includes --wait).",
|
|
348
474
|
)
|
|
349
475
|
@click.option(
|
|
350
476
|
"--polling-interval",
|
|
@@ -354,30 +480,53 @@ def _get_violation_count(process_data: dict) -> str:
|
|
|
354
480
|
help="How many seconds to wait between status polls. Status polls are"
|
|
355
481
|
" cheap, so a higher polling interval is most likely not needed.",
|
|
356
482
|
)
|
|
357
|
-
@click.
|
|
483
|
+
@click.pass_context
|
|
358
484
|
def execute_command( # noqa: PLR0913
|
|
359
|
-
|
|
485
|
+
ctx: Context,
|
|
360
486
|
iri: str,
|
|
361
487
|
shape_graph: str,
|
|
488
|
+
query: str,
|
|
489
|
+
result_graph: str,
|
|
490
|
+
replace: bool,
|
|
491
|
+
ignore_graph: list[str],
|
|
362
492
|
id_only: bool,
|
|
363
493
|
wait: bool,
|
|
494
|
+
inspect: bool,
|
|
364
495
|
polling_interval: int,
|
|
365
496
|
) -> None:
|
|
366
497
|
"""Start a new validation process.
|
|
367
498
|
|
|
368
|
-
Validation is performed on all typed resources of
|
|
369
|
-
Each resource is validated against all applicable node
|
|
370
|
-
|
|
499
|
+
Validation is performed on all typed resources of the data / context graph
|
|
500
|
+
(and its sub-graphs). Each resource is validated against all applicable node
|
|
501
|
+
shapes from the shape catalog.
|
|
371
502
|
"""
|
|
372
|
-
|
|
373
|
-
if
|
|
503
|
+
app: ApplicationContext = ctx.obj
|
|
504
|
+
if id_only and inspect:
|
|
505
|
+
raise UsageError(
|
|
506
|
+
"Output can be the summary (default), the process ID (--id-only) "
|
|
507
|
+
"or the violation list (--inspect)."
|
|
508
|
+
)
|
|
509
|
+
process_id = validation.start(
|
|
510
|
+
context_graph=iri,
|
|
511
|
+
shape_graph=shape_graph,
|
|
512
|
+
query=get_query_text(query, {"resource"}) if query else None,
|
|
513
|
+
result_graph=result_graph,
|
|
514
|
+
replace=replace,
|
|
515
|
+
ignore_graph=ignore_graph,
|
|
516
|
+
)
|
|
517
|
+
if wait or inspect:
|
|
374
518
|
_wait_for_process_completion(
|
|
375
519
|
app=app, process_id=process_id, use_rich=not id_only, polling_interval=polling_interval
|
|
376
520
|
)
|
|
377
521
|
if id_only:
|
|
378
522
|
app.echo_info(process_id)
|
|
379
523
|
return
|
|
380
|
-
|
|
524
|
+
if inspect:
|
|
525
|
+
ctx.params["process_id"] = process_id
|
|
526
|
+
data = violations_list.apply_filters(ctx=ctx, filter_=[])
|
|
527
|
+
_print_violation_table(app=app, data_graph=iri, shape_graph=shape_graph, violations=data)
|
|
528
|
+
return
|
|
529
|
+
_print_process_summary(process_id=process_id, app=app)
|
|
381
530
|
|
|
382
531
|
|
|
383
532
|
@click.command(cls=CmemcCommand, name="list")
|
|
@@ -418,32 +567,25 @@ def list_command(ctx: Context, filter_: tuple[tuple[str, str]], id_only: bool, r
|
|
|
418
567
|
app.echo_info(_["id"])
|
|
419
568
|
return
|
|
420
569
|
|
|
421
|
-
if len(validations) == 0:
|
|
422
|
-
app.echo_warning(
|
|
423
|
-
"No validation processes found. "
|
|
424
|
-
"Use `graph validation execute` to start a new validation process."
|
|
425
|
-
)
|
|
426
|
-
return
|
|
427
|
-
|
|
428
570
|
# output a user table
|
|
429
571
|
table = []
|
|
430
572
|
for _ in validations:
|
|
431
|
-
if "executionStarted" in _ and _["executionStarted"] is not None:
|
|
432
|
-
stamp = datetime.fromtimestamp(_["executionStarted"] / 1000, tz=UTC)
|
|
433
|
-
time_ago = timeago.format(stamp, datetime.now(tz=UTC))
|
|
434
|
-
else:
|
|
435
|
-
time_ago = f"{_['state']}"
|
|
436
573
|
row = [
|
|
437
574
|
_["id"],
|
|
438
575
|
_["state"],
|
|
439
|
-
|
|
576
|
+
_.get("executionStarted", None),
|
|
440
577
|
_["contextGraphIri"],
|
|
441
578
|
_get_resource_count(_),
|
|
442
579
|
_get_violation_count(_),
|
|
443
580
|
]
|
|
444
581
|
table.append(row)
|
|
445
582
|
app.echo_info_table(
|
|
446
|
-
table,
|
|
583
|
+
table,
|
|
584
|
+
headers=["ID", "Status", "Started", "Graph", "Resources", "Violations"],
|
|
585
|
+
caption=f"Validation Processes of {get_cmem_base_uri()}",
|
|
586
|
+
cell_processing={2: TimeAgo(), 3: GraphLink()},
|
|
587
|
+
empty_table_message="No validation processes found. "
|
|
588
|
+
"Use `graph validation execute` to start a new validation process.",
|
|
447
589
|
)
|
|
448
590
|
|
|
449
591
|
|
|
@@ -499,7 +641,7 @@ def inspect_command( # noqa: PLR0913
|
|
|
499
641
|
if raw:
|
|
500
642
|
app.echo_info_json(validation.get_aggregation(batch_id=process_id))
|
|
501
643
|
else:
|
|
502
|
-
|
|
644
|
+
_print_process_summary(app=app, process_id=process_id)
|
|
503
645
|
return
|
|
504
646
|
|
|
505
647
|
data = violations_list.apply_filters(ctx=ctx, filter_=filter_)
|
|
@@ -517,12 +659,15 @@ def inspect_command( # noqa: PLR0913
|
|
|
517
659
|
"The given validation process does not have any violations - "
|
|
518
660
|
"I will show the summary instead."
|
|
519
661
|
)
|
|
520
|
-
|
|
662
|
+
_print_process_summary(app=app, process_id=process_id)
|
|
521
663
|
else:
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
app
|
|
525
|
-
|
|
664
|
+
process_data = validation.get_aggregation(batch_id=process_id)
|
|
665
|
+
_print_violation_table(
|
|
666
|
+
app=app,
|
|
667
|
+
violations=data,
|
|
668
|
+
shape_graph=process_data["shapeGraphIri"],
|
|
669
|
+
data_graph=process_data["contextGraphIri"],
|
|
670
|
+
)
|
|
526
671
|
|
|
527
672
|
|
|
528
673
|
@click.command(cls=CmemcCommand, name="cancel")
|
|
@@ -535,58 +680,104 @@ def cancel_command(app: ApplicationContext, process_id: str) -> None:
|
|
|
535
680
|
processes, use the `graph validation list` command with the option
|
|
536
681
|
`--filter status running`, or utilize the tab completion of this command.
|
|
537
682
|
"""
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
if graph_validation["state"] != validation.STATUS_RUNNING:
|
|
547
|
-
raise click.UsageError(
|
|
548
|
-
f"Graph validation process with ID {process_id} is not a running anymore."
|
|
549
|
-
)
|
|
550
|
-
app.echo_info(f"Graph validation process with ID {process_id} ... ", nl=False)
|
|
683
|
+
all_status = {_["id"]: _["state"] for _ in validation.get_all_aggregations()}
|
|
684
|
+
if process_id not in all_status:
|
|
685
|
+
raise UsageError(f"Validation process with ID '{process_id}' is not known (anymore).")
|
|
686
|
+
if all_status[process_id] != validation.STATUS_RUNNING:
|
|
687
|
+
raise click.UsageError(
|
|
688
|
+
f"Validation process with ID '{process_id}' is not a running anymore."
|
|
689
|
+
)
|
|
690
|
+
app.echo_info(f"Validation process with ID '{process_id}' ... ", nl=False)
|
|
551
691
|
validation.cancel(batch_id=process_id)
|
|
552
692
|
app.echo_success("cancelled")
|
|
553
693
|
|
|
554
694
|
|
|
555
695
|
@click.command(cls=CmemcCommand, name="export")
|
|
556
|
-
@click.argument(
|
|
696
|
+
@click.argument(
|
|
697
|
+
"process_ids", nargs=-1, type=click.STRING, shell_complete=_complete_finished_batch_validations
|
|
698
|
+
)
|
|
699
|
+
@click.option(
|
|
700
|
+
"--output-file",
|
|
701
|
+
type=click.Path(writable=True, allow_dash=False, dir_okay=False),
|
|
702
|
+
default="report.xml",
|
|
703
|
+
show_default=True,
|
|
704
|
+
help="Export the report to this file. Existing files will be overwritten.",
|
|
705
|
+
)
|
|
706
|
+
@click.option(
|
|
707
|
+
"--exit-1",
|
|
708
|
+
type=click.Choice(["never", "error"]),
|
|
709
|
+
default="error",
|
|
710
|
+
show_default=True,
|
|
711
|
+
help="Specify, when this command returns with exit code 1. Available options are "
|
|
712
|
+
"'never' (exit 0, even if there are violations in the reports), "
|
|
713
|
+
"'error' (exit 1 if there is at least one violation in a report).), ",
|
|
714
|
+
)
|
|
557
715
|
@click.option(
|
|
558
716
|
"--format",
|
|
559
717
|
"format_",
|
|
560
718
|
type=click.Choice(["JSON", "XML"], case_sensitive=True),
|
|
561
|
-
default="
|
|
719
|
+
default="XML",
|
|
562
720
|
help="Export either the plain JSON report or a distilled jUnit XML report.",
|
|
563
721
|
show_default=True,
|
|
564
722
|
)
|
|
565
723
|
@click.pass_context
|
|
566
|
-
def export_command(
|
|
567
|
-
|
|
724
|
+
def export_command(
|
|
725
|
+
ctx: Context, process_ids: tuple[str], output_file: str, exit_1: str, format_: str
|
|
726
|
+
) -> None:
|
|
727
|
+
"""Export a report of finished validations.
|
|
568
728
|
|
|
569
|
-
This command exports
|
|
729
|
+
This command exports a jUnit XML or JSON report in order to process
|
|
570
730
|
them somewhere else (e.g. a CI pipeline).
|
|
571
731
|
|
|
732
|
+
You can export a single report of multiple validation processes.
|
|
733
|
+
|
|
734
|
+
For jUnit XML: Each validation process result will be transformed to
|
|
735
|
+
a single test suite. All violations of one resource in a result will be
|
|
736
|
+
collected and attached to a single test case in that test suite.
|
|
737
|
+
|
|
572
738
|
Note: Validation processes IDs can be listed with the `graph validation list`
|
|
573
739
|
command, or by utilizing the tab completion of this command.
|
|
574
740
|
"""
|
|
741
|
+
if len(process_ids) == 0:
|
|
742
|
+
raise UsageError("This command needs at least one validation process ID.")
|
|
575
743
|
app: ApplicationContext = ctx.obj
|
|
576
|
-
|
|
744
|
+
process_ids_to_test = {_: True for _ in process_ids}
|
|
745
|
+
overall_violations = 0
|
|
746
|
+
overall_resources = 0
|
|
577
747
|
for _ in validation.get_all_aggregations():
|
|
578
|
-
if _["id"]
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
748
|
+
if _["id"] in process_ids_to_test:
|
|
749
|
+
if _["state"] != "FINISHED":
|
|
750
|
+
raise UsageError(f"Validation process with ID '{_['id']}' is still running.")
|
|
751
|
+
del process_ids_to_test[_["id"]]
|
|
752
|
+
overall_violations += int(_["violationsCount"])
|
|
753
|
+
overall_resources += int(_["resourceProcessedCount"])
|
|
754
|
+
if len(process_ids_to_test) > 0:
|
|
755
|
+
raise UsageError(
|
|
756
|
+
"Validation processes with the following IDs not known (anymore): "
|
|
757
|
+
+ ", ".join(process_ids_to_test)
|
|
758
|
+
)
|
|
759
|
+
reports = []
|
|
760
|
+
for process_id in process_ids:
|
|
761
|
+
report = validation.get(batch_id=process_id)
|
|
762
|
+
reports.append(report)
|
|
763
|
+
app.echo_info(
|
|
764
|
+
f"Export of {len(reports)} validation report(s) with"
|
|
765
|
+
f" {overall_violations} violations in {overall_resources} resources"
|
|
766
|
+
f" to {output_file} ... ",
|
|
767
|
+
nl=False,
|
|
768
|
+
)
|
|
769
|
+
with Path(output_file, mode="w", encoding="utf-8") as file:
|
|
770
|
+
if format_ == "XML":
|
|
771
|
+
file.write_text(_reports_to_junit(reports))
|
|
772
|
+
if format_ == "JSON":
|
|
773
|
+
file.write_text(json.dumps(reports, indent=2))
|
|
774
|
+
app.echo_success("done")
|
|
775
|
+
if exit_1 == "error" and overall_violations > 0:
|
|
776
|
+
app.echo_error(
|
|
777
|
+
"Exit 1 since violations where found in the reports "
|
|
778
|
+
"(can be suppressed with '--exit-1 never')."
|
|
779
|
+
)
|
|
780
|
+
sys.exit(1)
|
|
590
781
|
|
|
591
782
|
|
|
592
783
|
@click.group(cls=CmemcGroup, name="validation")
|
cmem_cmemc/commands/variable.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""DataIntegration variable commands for cmemc."""
|
|
2
|
+
|
|
2
3
|
import re
|
|
3
4
|
|
|
4
5
|
import click
|
|
@@ -11,7 +12,8 @@ from cmem.cmempy.workspace.projects.variables import (
|
|
|
11
12
|
)
|
|
12
13
|
|
|
13
14
|
from cmem_cmemc import completion
|
|
14
|
-
from cmem_cmemc.
|
|
15
|
+
from cmem_cmemc.command import CmemcCommand
|
|
16
|
+
from cmem_cmemc.command_group import CmemcGroup
|
|
15
17
|
from cmem_cmemc.context import ApplicationContext
|
|
16
18
|
from cmem_cmemc.utils import check_or_select_project, split_task_id
|
|
17
19
|
|
|
@@ -102,7 +104,13 @@ def list_command(
|
|
|
102
104
|
_.get("description", ""),
|
|
103
105
|
]
|
|
104
106
|
table.append(row)
|
|
105
|
-
app.echo_info_table(
|
|
107
|
+
app.echo_info_table(
|
|
108
|
+
table,
|
|
109
|
+
headers=headers,
|
|
110
|
+
sort_column=0,
|
|
111
|
+
empty_table_message="No project variables found. "
|
|
112
|
+
"Use the `project variable create` command to create a new project variable.",
|
|
113
|
+
)
|
|
106
114
|
|
|
107
115
|
|
|
108
116
|
@click.command(cls=CmemcCommand, name="get")
|