cmem-cmemc 24.2.0rc2__py3-none-any.whl → 24.3.0rc2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. cmem_cmemc/__init__.py +7 -12
  2. cmem_cmemc/command.py +20 -0
  3. cmem_cmemc/command_group.py +70 -0
  4. cmem_cmemc/commands/__init__.py +0 -81
  5. cmem_cmemc/commands/acl.py +118 -62
  6. cmem_cmemc/commands/admin.py +46 -35
  7. cmem_cmemc/commands/client.py +2 -1
  8. cmem_cmemc/commands/config.py +3 -1
  9. cmem_cmemc/commands/dataset.py +27 -24
  10. cmem_cmemc/commands/graph.py +160 -19
  11. cmem_cmemc/commands/metrics.py +195 -79
  12. cmem_cmemc/commands/migration.py +267 -0
  13. cmem_cmemc/commands/project.py +62 -17
  14. cmem_cmemc/commands/python.py +56 -25
  15. cmem_cmemc/commands/query.py +23 -14
  16. cmem_cmemc/commands/resource.py +10 -2
  17. cmem_cmemc/commands/scheduler.py +10 -2
  18. cmem_cmemc/commands/store.py +118 -14
  19. cmem_cmemc/commands/user.py +8 -2
  20. cmem_cmemc/commands/validation.py +165 -78
  21. cmem_cmemc/commands/variable.py +10 -2
  22. cmem_cmemc/commands/vocabulary.py +48 -29
  23. cmem_cmemc/commands/workflow.py +86 -59
  24. cmem_cmemc/commands/workspace.py +27 -8
  25. cmem_cmemc/completion.py +190 -140
  26. cmem_cmemc/constants.py +2 -0
  27. cmem_cmemc/context.py +88 -42
  28. cmem_cmemc/manual_helper/graph.py +1 -0
  29. cmem_cmemc/manual_helper/multi_page.py +3 -1
  30. cmem_cmemc/migrations/__init__.py +1 -0
  31. cmem_cmemc/migrations/abc.py +84 -0
  32. cmem_cmemc/migrations/access_conditions_243.py +122 -0
  33. cmem_cmemc/migrations/bootstrap_data.py +28 -0
  34. cmem_cmemc/migrations/shapes_widget_integrations_243.py +274 -0
  35. cmem_cmemc/migrations/workspace_configurations.py +28 -0
  36. cmem_cmemc/object_list.py +53 -22
  37. cmem_cmemc/parameter_types/__init__.py +1 -0
  38. cmem_cmemc/parameter_types/path.py +69 -0
  39. cmem_cmemc/smart_path/__init__.py +94 -0
  40. cmem_cmemc/smart_path/clients/__init__.py +63 -0
  41. cmem_cmemc/smart_path/clients/http.py +65 -0
  42. cmem_cmemc/string_processor.py +83 -0
  43. cmem_cmemc/title_helper.py +41 -0
  44. cmem_cmemc/utils.py +100 -45
  45. {cmem_cmemc-24.2.0rc2.dist-info → cmem_cmemc-24.3.0rc2.dist-info}/LICENSE +1 -1
  46. cmem_cmemc-24.3.0rc2.dist-info/METADATA +89 -0
  47. cmem_cmemc-24.3.0rc2.dist-info/RECORD +53 -0
  48. {cmem_cmemc-24.2.0rc2.dist-info → cmem_cmemc-24.3.0rc2.dist-info}/WHEEL +1 -1
  49. cmem_cmemc-24.2.0rc2.dist-info/METADATA +0 -69
  50. cmem_cmemc-24.2.0rc2.dist-info/RECORD +0 -37
  51. {cmem_cmemc-24.2.0rc2.dist-info → cmem_cmemc-24.3.0rc2.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 UTC, datetime
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, get_dp_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.1.0"
30
+ DI_TARGET_VERSION = "v24.2.0"
28
31
 
29
- DP_TARGET_VERSION = "v24.1.0"
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=UTC).date())
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
- self.config_dir = Path(click.get_app_dir(self.app_name))
112
- self.config_dir.mkdir(parents=True, exist_ok=True)
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().name}")
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 default connection.")
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(config_file, encoding="utf-8")
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
- if cmemc_complete is not None:
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(message: str, nl: bool = True, err: bool = True) -> None:
257
- """Output an error message."""
258
- # pylint: disable=invalid-name
259
- click.secho(message, fg="red", err=err, nl=nl)
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
- def echo_debug(self, message: str) -> None:
262
- """Output a debug message if --debug is enabled."""
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=UTC)
266
- click.secho(f"[{now!s}] {message}", err=True, dim=True)
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(self, message: str | list[str] | set[str], nl: bool = True, fg: str = "") -> None:
269
- """Output one or more info messages, if not suppressed by --quiet."""
270
- # pylint: disable=invalid-name
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 DP version
421
+ # check Explore version
376
422
  self.check_concrete_version(
377
- name="DataPlatform", version=get_dp_version(), target_version=DP_TARGET_VERSION
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, # noqa: S603
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 help text and command structure graph."""
2
+
2
3
  import contextlib
3
4
 
4
5
  import click.core
@@ -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,122 @@
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 NOT EXISTS { ?subject shui:isSystemResource true }
24
+ FILTER (?class IN (auth:AccessCondition, shui:SparqlQuery) ) .
25
+ }
26
+ """
27
+ move_query = """{{DEFAULT_PREFIXES}}
28
+ DELETE { GRAPH <urn:elds-backend-access-conditions-graph> { ?s ?p ?o . } }
29
+ INSERT { GRAPH <https://ns.eccenca.com/data/ac/> { ?s ?p ?o . } }
30
+ WHERE {
31
+ GRAPH <urn:elds-backend-access-conditions-graph> {
32
+ ?s ?p ?o .
33
+ ?s a ?class .
34
+ FILTER (?class IN (auth:AccessCondition, shui:SparqlQuery) ) .
35
+ FILTER NOT EXISTS { ?s shui:isSystemResource true }
36
+ }
37
+ }
38
+ """
39
+
40
+ def is_applicable(self) -> bool:
41
+ """Test if the recipe can be applied."""
42
+ old_acls = self._select(self.check_query)
43
+ return len(old_acls) > 0
44
+
45
+ def apply(self) -> None:
46
+ """Apply the recipe to the current version."""
47
+ self._update(self.move_query)
48
+ refresh_acls()
49
+
50
+
51
+ class RenameAuthVocabularyResources(MigrationRecipe):
52
+ """24.4 Access Conditions Migration"""
53
+
54
+ id = "acl-vocab-24.3"
55
+ description = "Migrate auth vocabulary terms (actions and other grants)"
56
+ component: components = "explore"
57
+ first_version = "24.3"
58
+ tags: ClassVar[list[str]] = ["system", "acl"]
59
+ check_query = """{{DEFAULT_PREFIXES}}
60
+ SELECT DISTINCT ?acl_id
61
+ WHERE {
62
+ GRAPH ?acl_graph {
63
+ ?acl_id a auth:AccessCondition .
64
+ FILTER NOT EXISTS { ?acl_id shui:isSystemResource true }
65
+ ?acl_id ?property ?old_term .
66
+ FILTER (?old_term in (
67
+ <urn:elds-backend-all-actions>,
68
+ <urn:elds-backend-actions-auth-access-control>,
69
+ <urn:eccenca:di>,
70
+ <urn:eccenca:ThesaurusUserInterface>,
71
+ <urn:eccenca:AccessInternalGraphs>,
72
+ <urn:eccenca:QueryUserInterface>,
73
+ <urn:eccenca:VocabularyUserInterface>,
74
+ <urn:eccenca:ExploreUserInterface>,
75
+ <https://vocab.eccenca.com/auth/Action/Viz/Manage>,
76
+ <https://vocab.eccenca.com/auth/Action/Viz/Read>,
77
+ <urn:elds-backend-anonymous-user>,
78
+ <urn:elds-backend-public-group>,
79
+ <urn:elds-backend-all-graphs>
80
+ ))
81
+ }
82
+ FILTER (?acl_graph IN (<urn:elds-backend-access-conditions-graph>, <https://ns.eccenca.com/data/ac/>))
83
+ }
84
+ """
85
+ move_query = """{{DEFAULT_PREFIXES}}
86
+ DELETE { GRAPH ?acl_graph { ?s ?p <{{OLD_IRI}}> . } }
87
+ INSERT { GRAPH ?acl_graph { ?s ?p <{{NEW_IRI}}> . } }
88
+ WHERE {
89
+ GRAPH ?acl_graph {
90
+ ?s ?p ?o .
91
+ FILTER NOT EXISTS { ?s shui:isSystemResource true }
92
+ FILTER (?o = <{{OLD_IRI}}> )
93
+ }
94
+ FILTER (?acl_graph IN (<urn:elds-backend-access-conditions-graph>, <https://ns.eccenca.com/data/ac/>))
95
+ }
96
+ """
97
+ terms: ClassVar[dict[str, str]] = {
98
+ "urn:elds-backend-all-actions": "https://vocab.eccenca.com/auth/Action/AllActions",
99
+ "urn:elds-backend-actions-auth-access-control": "https://vocab.eccenca.com/auth/Action/ChangeAccessConditions",
100
+ "urn:eccenca:di": "https://vocab.eccenca.com/auth/Action/ChangeAccessConditions",
101
+ "urn:eccenca:ThesaurusUserInterface": "https://vocab.eccenca.com/auth/Action/Explore-ThesaurusCatalog",
102
+ "urn:eccenca:AccessInternalGraphs": "https://vocab.eccenca.com/auth/Action/Explore-ListSystemGraphs",
103
+ "urn:eccenca:QueryUserInterface": "https://vocab.eccenca.com/auth/Action/Explore-QueryCatalog",
104
+ "urn:eccenca:VocabularyUserInterface": "https://vocab.eccenca.com/auth/Action/Explore-VocabularyCatalog",
105
+ "urn:eccenca:ExploreUserInterface": "https://vocab.eccenca.com/auth/Action/Explore-KnowledgeGraphs",
106
+ "https://vocab.eccenca.com/auth/Action/Viz/Manage": "https://vocab.eccenca.com/auth/Action/Explore-BKE-Manage",
107
+ "https://vocab.eccenca.com/auth/Action/Viz/Read": "https://vocab.eccenca.com/auth/Action/Explore-BKE-Read",
108
+ "urn:elds-backend-anonymous-user": "https://vocab.eccenca.com/auth/AnonymousUser",
109
+ "urn:elds-backend-public-group": "https://vocab.eccenca.com/auth/PublicGroup",
110
+ "urn:elds-backend-all-graphs": "https://vocab.eccenca.com/auth/AllGraphs",
111
+ }
112
+
113
+ def is_applicable(self) -> bool:
114
+ """Test if the recipe can be applied."""
115
+ acls_with_vocabs_to_change = self._select(self.check_query)
116
+ return len(acls_with_vocabs_to_change) > 0
117
+
118
+ def apply(self) -> None:
119
+ """Apply the recipe to the current version."""
120
+ for old_iri, new_iri in self.terms.items():
121
+ self._update(self.move_query, placeholder={"OLD_IRI": old_iri, "NEW_IRI": new_iri})
122
+ refresh_acls()
@@ -0,0 +1,28 @@
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
+ shapes_status = status_info["shapes"]["version"]
24
+ return bool(shapes_status != status_info["explore"]["version"])
25
+
26
+ def apply(self) -> None:
27
+ """Apply the recipe to the current version."""
28
+ import_bootstrap_data()