cmem-cmemc 24.2.0rc2__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 +165 -78
- 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.0rc2.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.0rc2.dist-info → cmem_cmemc-24.3.0rc1.dist-info}/WHEEL +1 -1
- cmem_cmemc-24.2.0rc2.dist-info/METADATA +0 -69
- cmem_cmemc-24.2.0rc2.dist-info/RECORD +0 -37
- {cmem_cmemc-24.2.0rc2.dist-info → cmem_cmemc-24.3.0rc1.dist-info}/entry_points.txt +0 -0
cmem_cmemc/constants.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""cmemc constants"""
|
|
2
|
+
|
|
2
3
|
NAMESPACES = {
|
|
3
4
|
"void": "http://rdfs.org/ns/void#",
|
|
4
5
|
"di": "https://vocab.eccenca.com/di/",
|
|
@@ -11,3 +12,4 @@ NAMESPACES = {
|
|
|
11
12
|
NS_ACL = "http://eccenca.com/ac/"
|
|
12
13
|
NS_USER = "http://eccenca.com/"
|
|
13
14
|
NS_GROUP = "http://eccenca.com/"
|
|
15
|
+
NS_ACTION = "https://vocab.eccenca.com/auth/Action/"
|
cmem_cmemc/context.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""The main command line interface."""
|
|
2
|
+
|
|
2
3
|
import ast
|
|
3
4
|
import configparser
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import subprocess # nosec
|
|
8
|
-
from datetime import
|
|
9
|
+
from datetime import datetime, timezone
|
|
9
10
|
from os import environ as env
|
|
10
11
|
from os import getenv
|
|
11
12
|
from pathlib import Path
|
|
@@ -14,19 +15,21 @@ from shutil import which
|
|
|
14
15
|
import click
|
|
15
16
|
import cmem.cmempy.config as cmempy_config
|
|
16
17
|
import urllib3
|
|
17
|
-
from cmem.cmempy.health import get_di_version,
|
|
18
|
+
from cmem.cmempy.health import get_di_version, get_explore_version
|
|
18
19
|
from pygments import highlight
|
|
19
20
|
from pygments.formatters import get_formatter_by_name
|
|
20
21
|
from pygments.lexers import get_lexer_by_name
|
|
21
22
|
from rich import box
|
|
22
23
|
from rich.console import Console
|
|
23
24
|
from rich.table import Table
|
|
25
|
+
from urllib3.exceptions import InsecureRequestWarning
|
|
24
26
|
|
|
25
27
|
from cmem_cmemc.exceptions import InvalidConfigurationError
|
|
28
|
+
from cmem_cmemc.string_processor import StringProcessor, process_row
|
|
26
29
|
|
|
27
|
-
DI_TARGET_VERSION = "v24.
|
|
30
|
+
DI_TARGET_VERSION = "v24.2.0"
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
EXPLORE_TARGET_VERSION = "v24.2.0"
|
|
30
33
|
|
|
31
34
|
KNOWN_CONFIG_KEYS = {
|
|
32
35
|
"CMEM_BASE_URI": cmempy_config.get_cmem_base_uri,
|
|
@@ -47,6 +50,8 @@ KNOWN_CONFIG_KEYS = {
|
|
|
47
50
|
|
|
48
51
|
KNOWN_SECRET_KEYS = ("OAUTH_PASSWORD", "OAUTH_CLIENT_SECRET", "OAUTH_ACCESS_TOKEN")
|
|
49
52
|
|
|
53
|
+
SSL_VERIFY_WARNING = "SSL verification is disabled (SSL_VERIFY=False)."
|
|
54
|
+
|
|
50
55
|
|
|
51
56
|
class ApplicationContext:
|
|
52
57
|
"""Context of the command line interface."""
|
|
@@ -90,7 +95,7 @@ class ApplicationContext:
|
|
|
90
95
|
def get_template_data(self) -> dict[str, str]:
|
|
91
96
|
"""Get the template data dict with vars from the context."""
|
|
92
97
|
data: dict[str, str] = {}
|
|
93
|
-
today = str(datetime.now(tz=
|
|
98
|
+
today = str(datetime.now(tz=timezone.utc).date())
|
|
94
99
|
data.update(date=today)
|
|
95
100
|
if self.connection is not None:
|
|
96
101
|
data.update(connection=self.connection.name)
|
|
@@ -108,8 +113,11 @@ class ApplicationContext:
|
|
|
108
113
|
|
|
109
114
|
def set_config_dir(self) -> None:
|
|
110
115
|
"""Set the configuration directory"""
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
try:
|
|
117
|
+
self.config_dir = Path(click.get_app_dir(self.app_name))
|
|
118
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
except (OSError, PermissionError, FileNotFoundError):
|
|
120
|
+
self.echo_debug(f"Could not create config directory {self.config_dir}")
|
|
113
121
|
|
|
114
122
|
def set_config_file(self, config_file: str | None = None) -> None:
|
|
115
123
|
"""Set and return the context config file"""
|
|
@@ -117,7 +125,7 @@ class ApplicationContext:
|
|
|
117
125
|
self.config_file = Path(config_file)
|
|
118
126
|
else:
|
|
119
127
|
self.config_file = self.config_dir / "config.ini"
|
|
120
|
-
self.echo_debug(f"Set config to {self.config_file.absolute()
|
|
128
|
+
self.echo_debug(f"Set config to {self.config_file.absolute()}")
|
|
121
129
|
|
|
122
130
|
def configure_cmempy(self, config: configparser.SectionProxy | None = None) -> None:
|
|
123
131
|
"""Configure the cmempy API to use a new connection."""
|
|
@@ -142,12 +150,6 @@ class ApplicationContext:
|
|
|
142
150
|
self.set_credential_from_process(_, _ + "_PROCESS", config)
|
|
143
151
|
|
|
144
152
|
self.echo_debug(f"CA bundle loaded from {cmempy_config.get_requests_ca_bundle()}")
|
|
145
|
-
# If cert validation is disabled, output a warning
|
|
146
|
-
# Also disable library warnings:
|
|
147
|
-
# https://urllib3.readthedocs.io/en/latest/advanced-usage.html
|
|
148
|
-
if not cmempy_config.get_ssl_verify():
|
|
149
|
-
self.echo_warning("SSL verification is disabled (SSL_VERIFY=False).")
|
|
150
|
-
urllib3.disable_warnings()
|
|
151
153
|
return
|
|
152
154
|
|
|
153
155
|
def set_connection_from_params(self, params: dict) -> None:
|
|
@@ -192,7 +194,7 @@ class ApplicationContext:
|
|
|
192
194
|
self.config = self.get_config()
|
|
193
195
|
self.connection = None
|
|
194
196
|
if section_string is None or section_string == "":
|
|
195
|
-
self.echo_debug("No config given, use API
|
|
197
|
+
self.echo_debug("No config given, use API defaults or environment connection.")
|
|
196
198
|
elif section_string not in self.config:
|
|
197
199
|
raise InvalidConfigurationError(
|
|
198
200
|
f"There is no connection '{section_string}' configured in "
|
|
@@ -202,6 +204,14 @@ class ApplicationContext:
|
|
|
202
204
|
self.echo_debug(f"Use connection config: {section_string}")
|
|
203
205
|
self.connection = self.config[section_string]
|
|
204
206
|
self.configure_cmempy(self.connection)
|
|
207
|
+
|
|
208
|
+
# If cert validation is disabled, output a warning
|
|
209
|
+
# Also disable library warnings:
|
|
210
|
+
# https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings
|
|
211
|
+
if not cmempy_config.get_ssl_verify():
|
|
212
|
+
self.echo_warning(SSL_VERIFY_WARNING)
|
|
213
|
+
urllib3.disable_warnings(category=InsecureRequestWarning)
|
|
214
|
+
|
|
205
215
|
return self.connection
|
|
206
216
|
|
|
207
217
|
def get_config_file(self) -> Path:
|
|
@@ -214,17 +224,18 @@ class ApplicationContext:
|
|
|
214
224
|
|
|
215
225
|
def get_config(self) -> configparser.RawConfigParser:
|
|
216
226
|
"""Parse the configuration"""
|
|
227
|
+
config = configparser.RawConfigParser()
|
|
217
228
|
try:
|
|
218
|
-
config = configparser.RawConfigParser()
|
|
219
|
-
config_file = self.get_config_file()
|
|
220
229
|
# https://stackoverflow.com/questions/1648517/
|
|
221
|
-
config.read(
|
|
230
|
+
config.read(self.get_config_file(), encoding="utf-8")
|
|
222
231
|
except configparser.Error as error:
|
|
223
232
|
raise InvalidConfigurationError(
|
|
224
233
|
"The following config parser error needs to be fixed with your config file:\n"
|
|
225
234
|
f"{error!s}\n"
|
|
226
235
|
"You can use the 'config edit' command to fix this."
|
|
227
236
|
) from error
|
|
237
|
+
except Exception as error: # noqa: BLE001
|
|
238
|
+
self.echo_debug(f"Could not read config file - provide empty config: {error!s}")
|
|
228
239
|
return config
|
|
229
240
|
|
|
230
241
|
@staticmethod
|
|
@@ -241,33 +252,59 @@ class ApplicationContext:
|
|
|
241
252
|
cmemc_complete = os.getenv("_CMEMC_COMPLETE", default=None)
|
|
242
253
|
if comp_words is not None:
|
|
243
254
|
return True
|
|
244
|
-
|
|
245
|
-
return True
|
|
246
|
-
return False
|
|
255
|
+
return cmemc_complete is not None
|
|
247
256
|
|
|
248
257
|
@staticmethod
|
|
249
|
-
def echo_warning(message: str, nl: bool = True) -> None:
|
|
258
|
+
def echo_warning(message: str, nl: bool = True, condition: bool = True) -> None:
|
|
250
259
|
"""Output a warning message."""
|
|
260
|
+
if not condition:
|
|
261
|
+
return
|
|
251
262
|
if ApplicationContext.is_completing():
|
|
252
263
|
return
|
|
253
264
|
click.secho(message, fg="yellow", err=True, nl=nl)
|
|
254
265
|
|
|
255
266
|
@staticmethod
|
|
256
|
-
def echo_error(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
267
|
+
def echo_error(
|
|
268
|
+
message: str | list[str], nl: bool = True, err: bool = True, prepend_line: bool = False
|
|
269
|
+
) -> None:
|
|
270
|
+
"""Output an error message.
|
|
260
271
|
|
|
261
|
-
|
|
262
|
-
|
|
272
|
+
2024-05-17: also allows list of strings now
|
|
273
|
+
2024-05-17: new prepend_line parameter
|
|
274
|
+
"""
|
|
263
275
|
# pylint: disable=invalid-name
|
|
276
|
+
click.echo("") if prepend_line is True else None
|
|
277
|
+
messages: list[str] = [message] if isinstance(message, str) else message
|
|
278
|
+
for _ in messages:
|
|
279
|
+
click.secho(_, fg="red", err=err, nl=nl)
|
|
280
|
+
|
|
281
|
+
def echo_debug(self, message: str | list[str]) -> None:
|
|
282
|
+
"""Output a debug message if --debug is enabled.
|
|
283
|
+
|
|
284
|
+
2024-05-17: also allows list of strings now
|
|
285
|
+
"""
|
|
286
|
+
messages: list[str] = [message] if isinstance(message, str) else message
|
|
264
287
|
if self.debug:
|
|
265
|
-
now = datetime.now(tz=
|
|
266
|
-
|
|
288
|
+
now = datetime.now(tz=timezone.utc)
|
|
289
|
+
for _ in messages:
|
|
290
|
+
click.secho(f"[{now!s}] {_}", err=True, dim=True)
|
|
267
291
|
|
|
268
|
-
def echo_info(
|
|
269
|
-
|
|
270
|
-
|
|
292
|
+
def echo_info(
|
|
293
|
+
self,
|
|
294
|
+
message: str | list[str] | set[str],
|
|
295
|
+
nl: bool = True,
|
|
296
|
+
fg: str = "",
|
|
297
|
+
condition: bool = True,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""Output one or more info messages, if not suppressed by --quiet.
|
|
300
|
+
|
|
301
|
+
message: the string to output
|
|
302
|
+
nl: True if newlines need to be added (default)
|
|
303
|
+
fg: color
|
|
304
|
+
condition: do nothing if False
|
|
305
|
+
"""
|
|
306
|
+
if not condition:
|
|
307
|
+
return
|
|
271
308
|
if self.quiet:
|
|
272
309
|
return
|
|
273
310
|
if isinstance(message, str):
|
|
@@ -279,7 +316,6 @@ class ApplicationContext:
|
|
|
279
316
|
|
|
280
317
|
def echo_info_json(self, object_: object) -> None:
|
|
281
318
|
"""Output a formatted and highlighted json as info message."""
|
|
282
|
-
# pylint: disable=invalid-name
|
|
283
319
|
message = highlight(
|
|
284
320
|
json.dumps(object_, indent=2),
|
|
285
321
|
get_lexer_by_name("json"),
|
|
@@ -289,7 +325,6 @@ class ApplicationContext:
|
|
|
289
325
|
|
|
290
326
|
def echo_info_xml(self, document: str) -> None:
|
|
291
327
|
"""Output a formatted and highlighted XML as info message."""
|
|
292
|
-
# pylint: disable=invalid-name
|
|
293
328
|
message = highlight(
|
|
294
329
|
document,
|
|
295
330
|
get_lexer_by_name("xml"),
|
|
@@ -297,17 +332,28 @@ class ApplicationContext:
|
|
|
297
332
|
)
|
|
298
333
|
self.echo_info(message)
|
|
299
334
|
|
|
300
|
-
def echo_info_table(
|
|
335
|
+
def echo_info_table( # noqa: PLR0913
|
|
301
336
|
self,
|
|
302
337
|
rows: list,
|
|
303
338
|
headers: list[str],
|
|
304
339
|
sort_column: int | None = None,
|
|
305
340
|
caption: str | None = None,
|
|
341
|
+
cell_processing: dict[int, StringProcessor] | None = None,
|
|
342
|
+
empty_table_message: str | None = None,
|
|
306
343
|
) -> None:
|
|
307
344
|
"""Output a formatted and highlighted table as info message."""
|
|
308
345
|
self.update_console_width()
|
|
346
|
+
|
|
347
|
+
if len(rows) == 0 and empty_table_message:
|
|
348
|
+
self.echo_warning(empty_table_message)
|
|
349
|
+
return
|
|
350
|
+
|
|
309
351
|
if sort_column is not None:
|
|
310
352
|
rows = sorted(rows, key=lambda k: k[sort_column].lower())
|
|
353
|
+
|
|
354
|
+
if cell_processing:
|
|
355
|
+
rows = [process_row(row, cell_processing) for row in rows]
|
|
356
|
+
|
|
311
357
|
table = Table(
|
|
312
358
|
box=box.HEAVY,
|
|
313
359
|
row_styles=["bold", ""],
|
|
@@ -330,10 +376,10 @@ class ApplicationContext:
|
|
|
330
376
|
)
|
|
331
377
|
self.echo_info(message)
|
|
332
378
|
|
|
333
|
-
def echo_success(self, message: str, nl: bool = True) -> None:
|
|
379
|
+
def echo_success(self, message: str, nl: bool = True, condition: bool = True) -> None:
|
|
334
380
|
"""Output success message, if not suppressed by --quiet."""
|
|
335
381
|
# pylint: disable=invalid-name
|
|
336
|
-
self.echo_info(message, fg="green", nl=nl)
|
|
382
|
+
self.echo_info(message, fg="green", nl=nl, condition=condition)
|
|
337
383
|
|
|
338
384
|
@staticmethod
|
|
339
385
|
def echo_result(message: str, nl: bool = True) -> None:
|
|
@@ -372,9 +418,9 @@ class ApplicationContext:
|
|
|
372
418
|
|
|
373
419
|
def check_versions(self) -> None:
|
|
374
420
|
"""Check server versions against supported versions."""
|
|
375
|
-
# check
|
|
421
|
+
# check Explore version
|
|
376
422
|
self.check_concrete_version(
|
|
377
|
-
name="
|
|
423
|
+
name="Explore", version=get_explore_version(), target_version=EXPLORE_TARGET_VERSION
|
|
378
424
|
)
|
|
379
425
|
self.check_concrete_version(
|
|
380
426
|
name="DataIntegration", version=get_di_version(), target_version=DI_TARGET_VERSION
|
|
@@ -429,8 +475,8 @@ class ApplicationContext:
|
|
|
429
475
|
)
|
|
430
476
|
self.echo_debug(f"External credential process started {checked_command}")
|
|
431
477
|
split_output = (
|
|
432
|
-
subprocess.run( # nosec
|
|
433
|
-
checked_command,
|
|
478
|
+
subprocess.run( # nosec # noqa: S603
|
|
479
|
+
checked_command,
|
|
434
480
|
capture_output=True,
|
|
435
481
|
check=True,
|
|
436
482
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Generate a multi page documentation for documentation.eccenca.com."""
|
|
2
|
+
|
|
2
3
|
import re
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
@@ -16,10 +17,11 @@ def get_icon_for_command_group(full_name: str) -> str:
|
|
|
16
17
|
return {
|
|
17
18
|
"admin": "material/key-link",
|
|
18
19
|
"admin acl": "material/server-security",
|
|
20
|
+
"admin client": "material/account-cog",
|
|
19
21
|
"admin metrics": "material/chart-line-variant",
|
|
22
|
+
"admin migration": "material/database-arrow-up-outline",
|
|
20
23
|
"admin store": "material/database-outline",
|
|
21
24
|
"admin user": "material/account-cog",
|
|
22
|
-
"admin client": "material/account-cog",
|
|
23
25
|
"admin workspace": "material/folder-multiple-outline",
|
|
24
26
|
"admin workspace python": "material/language-python",
|
|
25
27
|
"config": "material/cog-outline",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Migration Recipes"""
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Migration recipe abstract base class"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import ClassVar, Literal
|
|
6
|
+
|
|
7
|
+
from cmem.cmempy.health import get_complete_status_info
|
|
8
|
+
from cmem.cmempy.queries import SparqlQuery
|
|
9
|
+
from packaging.version import Version
|
|
10
|
+
|
|
11
|
+
components = Literal["explore", "build"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MigrationRecipe(ABC):
|
|
15
|
+
"""Tests and migration functions to migrate a specific type of resource between versions"""
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
description: str
|
|
19
|
+
component: components = "explore"
|
|
20
|
+
first_version: str | None = None
|
|
21
|
+
last_version: str | None = None
|
|
22
|
+
tags: ClassVar[list[str]] = []
|
|
23
|
+
default_prefixes: ClassVar[dict[str, str]] = {
|
|
24
|
+
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
25
|
+
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
|
26
|
+
"sh": "http://www.w3.org/ns/shacl#",
|
|
27
|
+
"auth": "https://vocab.eccenca.com/auth/",
|
|
28
|
+
"shui": "https://vocab.eccenca.com/shui/",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def is_applicable(self) -> bool:
|
|
33
|
+
"""Test if the recipe can be applied."""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def apply(self) -> None:
|
|
37
|
+
"""Apply the recipe to the current version."""
|
|
38
|
+
|
|
39
|
+
def version_matches(self, component_version: str = "") -> bool:
|
|
40
|
+
"""Test if the component version matches the version specification of the recipe"""
|
|
41
|
+
status_info = get_complete_status_info()
|
|
42
|
+
first_version = self.first_version if self.first_version else "0.0.0"
|
|
43
|
+
last_version = self.last_version if self.last_version else "1000.0.0"
|
|
44
|
+
if component_version == "":
|
|
45
|
+
if self.component == "explore":
|
|
46
|
+
component_version = str(status_info["explore"]["version"])
|
|
47
|
+
if self.component == "build":
|
|
48
|
+
component_version = str(status_info["build"]["version"])
|
|
49
|
+
if component_version.startswith("v"):
|
|
50
|
+
component_version = component_version[1:]
|
|
51
|
+
# remove git describe stuff (24.2.1-501-gb0ca19b14)
|
|
52
|
+
component_version = re.sub(r"-.*", "", component_version)
|
|
53
|
+
if component_version == "":
|
|
54
|
+
return False
|
|
55
|
+
return Version(first_version) <= Version(component_version) <= Version(last_version)
|
|
56
|
+
|
|
57
|
+
def _get_default_prefixes(self) -> str:
|
|
58
|
+
"""Get default prefixes as SPARQL Snippet"""
|
|
59
|
+
snippet = ""
|
|
60
|
+
for prefix, namespace in self.default_prefixes.items():
|
|
61
|
+
snippet += f"PREFIX {prefix}: <{namespace}>\n"
|
|
62
|
+
return snippet
|
|
63
|
+
|
|
64
|
+
def _select(self, query_text: str, placeholder: dict[str, str] | None = None) -> list[dict]:
|
|
65
|
+
"""Get the bindings of a SPARQL Query result"""
|
|
66
|
+
if not placeholder:
|
|
67
|
+
placeholder = {"DEFAULT_PREFIXES": self._get_default_prefixes()}
|
|
68
|
+
else:
|
|
69
|
+
placeholder["DEFAULT_PREFIXES"] = self._get_default_prefixes()
|
|
70
|
+
query = SparqlQuery(query_text)
|
|
71
|
+
response: dict = query.get_json_results(placeholder=placeholder)
|
|
72
|
+
results: dict = response.get("results", {"bindings": []})
|
|
73
|
+
bindings: list[dict] = results.get("bindings") # type: ignore[assignment]
|
|
74
|
+
return bindings
|
|
75
|
+
|
|
76
|
+
def _update(self, query_text: str, placeholder: dict[str, str] | None = None) -> None:
|
|
77
|
+
"""Send an update query"""
|
|
78
|
+
if not placeholder:
|
|
79
|
+
placeholder = {"DEFAULT_PREFIXES": self._get_default_prefixes()}
|
|
80
|
+
else:
|
|
81
|
+
placeholder["DEFAULT_PREFIXES"] = self._get_default_prefixes()
|
|
82
|
+
query = SparqlQuery(query_text)
|
|
83
|
+
query.query_type = "UPDATE"
|
|
84
|
+
query.get_results(placeholder=placeholder)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Access Conditions migration recipes from before 24.3"""
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from cmem.cmempy.dp.authorization.refresh import get as refresh_acls
|
|
6
|
+
|
|
7
|
+
from cmem_cmemc.migrations.abc import MigrationRecipe, components
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MoveAccessConditionsToNewGraph(MigrationRecipe):
|
|
11
|
+
"""24.4 Access Conditions Migration"""
|
|
12
|
+
|
|
13
|
+
id = "acl-graph-24.3"
|
|
14
|
+
description = "Move access conditions and used queries to new ACL graph"
|
|
15
|
+
component: components = "explore"
|
|
16
|
+
first_version = "24.3"
|
|
17
|
+
tags: ClassVar[list[str]] = ["system", "acl"]
|
|
18
|
+
check_query = """{{DEFAULT_PREFIXES}}
|
|
19
|
+
SELECT ?subject
|
|
20
|
+
FROM <urn:elds-backend-access-conditions-graph>
|
|
21
|
+
WHERE {
|
|
22
|
+
?subject a ?class .
|
|
23
|
+
FILTER (?class IN (auth:AccessCondition, shui:SparqlQuery) ) .
|
|
24
|
+
}
|
|
25
|
+
"""
|
|
26
|
+
move_query = """{{DEFAULT_PREFIXES}}
|
|
27
|
+
DELETE { GRAPH <urn:elds-backend-access-conditions-graph> { ?s ?p ?o . } }
|
|
28
|
+
INSERT { GRAPH <https://ns.eccenca.com/data/ac/> { ?s ?p ?o . } }
|
|
29
|
+
WHERE {
|
|
30
|
+
GRAPH <urn:elds-backend-access-conditions-graph> {
|
|
31
|
+
?s ?p ?o .
|
|
32
|
+
?s a ?class .
|
|
33
|
+
FILTER (?class IN (auth:AccessCondition, shui:SparqlQuery) ) .
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def is_applicable(self) -> bool:
|
|
39
|
+
"""Test if the recipe can be applied."""
|
|
40
|
+
old_acls = self._select(self.check_query)
|
|
41
|
+
return len(old_acls) > 0
|
|
42
|
+
|
|
43
|
+
def apply(self) -> None:
|
|
44
|
+
"""Apply the recipe to the current version."""
|
|
45
|
+
self._update(self.move_query)
|
|
46
|
+
refresh_acls()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class RenameAuthVocabularyResources(MigrationRecipe):
|
|
50
|
+
"""24.4 Access Conditions Migration"""
|
|
51
|
+
|
|
52
|
+
id = "acl-vocab-24.3"
|
|
53
|
+
description = "Migrate auth vocabulary terms (actions and other grants)"
|
|
54
|
+
component: components = "explore"
|
|
55
|
+
first_version = "24.3"
|
|
56
|
+
tags: ClassVar[list[str]] = ["system", "acl"]
|
|
57
|
+
check_query = """{{DEFAULT_PREFIXES}}
|
|
58
|
+
SELECT DISTINCT ?acl_id
|
|
59
|
+
WHERE {
|
|
60
|
+
GRAPH ?acl_graph {
|
|
61
|
+
?acl_id a auth:AccessCondition .
|
|
62
|
+
?acl_id ?property ?old_term .
|
|
63
|
+
FILTER (?old_term in (
|
|
64
|
+
<urn:elds-backend-all-actions>,
|
|
65
|
+
<urn:elds-backend-actions-auth-access-control>,
|
|
66
|
+
<urn:eccenca:di>,
|
|
67
|
+
<urn:eccenca:ThesaurusUserInterface>,
|
|
68
|
+
<urn:eccenca:AccessInternalGraphs>,
|
|
69
|
+
<urn:eccenca:QueryUserInterface>,
|
|
70
|
+
<urn:eccenca:VocabularyUserInterface>,
|
|
71
|
+
<urn:eccenca:ExploreUserInterface>,
|
|
72
|
+
<https://vocab.eccenca.com/auth/Action/Viz/Manage>,
|
|
73
|
+
<https://vocab.eccenca.com/auth/Action/Viz/Read>,
|
|
74
|
+
<urn:elds-backend-anonymous-user>,
|
|
75
|
+
<urn:elds-backend-public-group>,
|
|
76
|
+
<urn:elds-backend-all-graphs>
|
|
77
|
+
))
|
|
78
|
+
}
|
|
79
|
+
FILTER (?acl_graph IN (<urn:elds-backend-access-conditions-graph>, <https://ns.eccenca.com/data/ac/>))
|
|
80
|
+
}
|
|
81
|
+
"""
|
|
82
|
+
move_query = """{{DEFAULT_PREFIXES}}
|
|
83
|
+
DELETE { GRAPH ?acl_graph { ?s ?p <{{OLD_IRI}}> . } }
|
|
84
|
+
INSERT { GRAPH ?acl_graph { ?s ?p <{{NEW_IRI}}> . } }
|
|
85
|
+
WHERE {
|
|
86
|
+
GRAPH ?acl_graph {
|
|
87
|
+
?s ?p ?o .
|
|
88
|
+
FILTER (?o = <{{OLD_IRI}}> )
|
|
89
|
+
}
|
|
90
|
+
FILTER (?acl_graph IN (<urn:elds-backend-access-conditions-graph>, <https://ns.eccenca.com/data/ac/>))
|
|
91
|
+
}
|
|
92
|
+
"""
|
|
93
|
+
terms: ClassVar[dict[str, str]] = {
|
|
94
|
+
"urn:elds-backend-all-actions": "https://vocab.eccenca.com/auth/Action/AllActions",
|
|
95
|
+
"urn:elds-backend-actions-auth-access-control": "https://vocab.eccenca.com/auth/Action/ChangeAccessConditions",
|
|
96
|
+
"urn:eccenca:di": "https://vocab.eccenca.com/auth/Action/ChangeAccessConditions",
|
|
97
|
+
"urn:eccenca:ThesaurusUserInterface": "https://vocab.eccenca.com/auth/Action/Explore-ThesaurusCatalog",
|
|
98
|
+
"urn:eccenca:AccessInternalGraphs": "https://vocab.eccenca.com/auth/Action/Explore-ListSystemGraphs",
|
|
99
|
+
"urn:eccenca:QueryUserInterface": "https://vocab.eccenca.com/auth/Action/Explore-QueryCatalog",
|
|
100
|
+
"urn:eccenca:VocabularyUserInterface": "https://vocab.eccenca.com/auth/Action/Explore-VocabularyCatalog",
|
|
101
|
+
"urn:eccenca:ExploreUserInterface": "https://vocab.eccenca.com/auth/Action/Explore-KnowledgeGraphs",
|
|
102
|
+
"https://vocab.eccenca.com/auth/Action/Viz/Manage": "https://vocab.eccenca.com/auth/Action/Explore-BKE-Manage",
|
|
103
|
+
"https://vocab.eccenca.com/auth/Action/Viz/Read": "https://vocab.eccenca.com/auth/Action/Explore-BKE-Read",
|
|
104
|
+
"urn:elds-backend-anonymous-user": "https://vocab.eccenca.com/auth/AnonymousUser",
|
|
105
|
+
"urn:elds-backend-public-group": "https://vocab.eccenca.com/auth/PublicGroup",
|
|
106
|
+
"urn:elds-backend-all-graphs": "https://vocab.eccenca.com/auth/AllGraphs",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def is_applicable(self) -> bool:
|
|
110
|
+
"""Test if the recipe can be applied."""
|
|
111
|
+
acls_with_vocabs_to_change = self._select(self.check_query)
|
|
112
|
+
return len(acls_with_vocabs_to_change) > 0
|
|
113
|
+
|
|
114
|
+
def apply(self) -> None:
|
|
115
|
+
"""Apply the recipe to the current version."""
|
|
116
|
+
for old_iri, new_iri in self.terms.items():
|
|
117
|
+
self._update(self.move_query, placeholder={"OLD_IRI": old_iri, "NEW_IRI": new_iri})
|
|
118
|
+
refresh_acls()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Bootstrap Data migration recipe"""
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from cmem.cmempy.dp.admin import import_bootstrap_data
|
|
6
|
+
from cmem.cmempy.health import get_complete_status_info
|
|
7
|
+
|
|
8
|
+
from cmem_cmemc.migrations.abc import MigrationRecipe, components
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MigrateBootstrapData(MigrationRecipe):
|
|
12
|
+
"""Update Bootstrap Data to current version"""
|
|
13
|
+
|
|
14
|
+
id = "bootstrap-data"
|
|
15
|
+
description = "Migrate bootstrap system data (same as admin store bootstrap --import)"
|
|
16
|
+
component: components = "explore"
|
|
17
|
+
first_version = "20.12"
|
|
18
|
+
tags: ClassVar[list[str]] = ["system", "shapes"]
|
|
19
|
+
|
|
20
|
+
def is_applicable(self) -> bool:
|
|
21
|
+
"""Test if the recipe can be applied."""
|
|
22
|
+
status_info = get_complete_status_info()
|
|
23
|
+
return status_info["shapes"]["version"] not in (
|
|
24
|
+
status_info["explore"]["version"],
|
|
25
|
+
"UNKNOWN",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def apply(self) -> None:
|
|
29
|
+
"""Apply the recipe to the current version."""
|
|
30
|
+
import_bootstrap_data()
|