cmem-cmemc 25.1.1__tar.gz → 25.2.0__tar.gz

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 (59) hide show
  1. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/PKG-INFO +2 -2
  2. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/command_group.py +1 -0
  3. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/admin.py +3 -3
  4. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/config.py +2 -2
  5. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/graph.py +149 -177
  6. cmem_cmemc-25.2.0/cmem_cmemc/commands/graph_imports.py +420 -0
  7. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/migration.py +8 -8
  8. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/project.py +1 -1
  9. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/python.py +5 -4
  10. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/query.py +2 -1
  11. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/resource.py +1 -1
  12. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/scheduler.py +1 -1
  13. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/store.py +1 -1
  14. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/variable.py +1 -1
  15. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/completion.py +23 -19
  16. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/constants.py +2 -0
  17. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/utils.py +52 -14
  18. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/pyproject.toml +3 -2
  19. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/LICENSE +0 -0
  20. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/README-public.md +0 -0
  21. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/__init__.py +0 -0
  22. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/_cmemc.zsh +0 -0
  23. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/cli.py +0 -0
  24. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/command.py +0 -0
  25. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/__init__.py +0 -0
  26. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/acl.py +0 -0
  27. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/client.py +0 -0
  28. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/dataset.py +0 -0
  29. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/manual.py +0 -0
  30. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/metrics.py +0 -0
  31. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/user.py +0 -0
  32. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/validation.py +0 -0
  33. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/vocabulary.py +0 -0
  34. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/workflow.py +0 -0
  35. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/commands/workspace.py +0 -0
  36. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/config_parser.py +0 -0
  37. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/context.py +0 -0
  38. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/exceptions.py +0 -0
  39. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/manual_helper/__init__.py +0 -0
  40. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/manual_helper/graph.py +0 -0
  41. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/manual_helper/multi_page.py +0 -0
  42. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/manual_helper/single_page.py +0 -0
  43. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/migrations/__init__.py +0 -0
  44. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/migrations/abc.py +0 -0
  45. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/migrations/access_conditions_243.py +0 -0
  46. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/migrations/bootstrap_data.py +0 -0
  47. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/migrations/remove_noop_triple_251.py +0 -0
  48. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/migrations/shapes_widget_integrations_243.py +0 -0
  49. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/migrations/sparql_query_texts_242.py +0 -0
  50. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/migrations/workspace_configurations.py +0 -0
  51. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/object_list.py +0 -0
  52. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/parameter_types/__init__.py +0 -0
  53. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/parameter_types/path.py +0 -0
  54. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/placeholder.py +0 -0
  55. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/smart_path/__init__.py +0 -0
  56. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/smart_path/clients/__init__.py +0 -0
  57. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/smart_path/clients/http.py +0 -0
  58. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/string_processor.py +0 -0
  59. {cmem_cmemc-25.1.1 → cmem_cmemc-25.2.0}/cmem_cmemc/title_helper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cmem-cmemc
3
- Version: 25.1.1
3
+ Version: 25.2.0
4
4
  Summary: Command line client for eccenca Corporate Memory
5
5
  License: Apache-2.0
6
6
  Author: eccenca
@@ -27,7 +27,7 @@ Classifier: Topic :: Software Development :: Testing
27
27
  Classifier: Topic :: Utilities
28
28
  Requires-Dist: beautifulsoup4 (>=4.13.3,<5.0.0)
29
29
  Requires-Dist: certifi (>=2024.2.2)
30
- Requires-Dist: click (>=8.1.8,<9.0.0)
30
+ Requires-Dist: click (>=8.1.8,<8.2.0)
31
31
  Requires-Dist: click-didyoumean (>=0.3.1,<0.4.0)
32
32
  Requires-Dist: click-help-colors (>=0.9.4,<0.10.0)
33
33
  Requires-Dist: cmem-cmempy (==25.1.0)
@@ -65,6 +65,7 @@ class CmemcGroup(HelpColorsGroup, DYMGroup):
65
65
  "validation": self.color_for_command_groups,
66
66
  "migrate": self.color_for_writing_commands,
67
67
  "migrations": self.color_for_command_groups,
68
+ "imports": self.color_for_command_groups,
68
69
  },
69
70
  )
70
71
  super().__init__(*args, **kwargs)
@@ -24,11 +24,11 @@ from cmem_cmemc.context import ApplicationContext
24
24
  from cmem_cmemc.utils import struct_to_table
25
25
 
26
26
  WARNING_MIGRATION = (
27
- "Your workspace configuration version does not match your DataPlatform version. "
27
+ "Your workspace configuration version does not match your Explore version. "
28
28
  "Please consider migrating your workspace configuration (admin store migrate command)."
29
29
  )
30
30
  WARNING_SHAPES = (
31
- "Your ShapeCatalog version does not match your DataPlatform version. "
31
+ "Your ShapeCatalog version does not match your Explore version. "
32
32
  "Please consider updating your bootstrap data (admin store boostrap command)."
33
33
  )
34
34
 
@@ -108,7 +108,7 @@ def status_command( # noqa: C901, PLR0912
108
108
  Additionally, this command informs you in one of these cases:
109
109
  (1) A warning, if the target version of your cmemc client is newer than the version
110
110
  of your backend.
111
- (2) A warning, if the ShapeCatalog has a different version than your DataPlatform component.
111
+ (2) A warning, if the ShapeCatalog has a different version than your Explore component.
112
112
  (3) An error, if your Corporate Memory license is expired (grace period).
113
113
  (4) A warning, if your Graph DB license will expire in less than a month.
114
114
 
@@ -119,8 +119,8 @@ def config() -> CmemcGroup: # type: ignore[empty-body]
119
119
 
120
120
  \b
121
121
  SSL_VERIFY=False - for ignoring certificate issues (not recommended)
122
- DP_API_ENDPOINT=URL - to point to a non-standard DataPlatform location
123
- DI_API_ENDPOINT=URL - to point to a non-standard DataIntegration location
122
+ DP_API_ENDPOINT=URL - to point to a non-standard Explore backend (DataPlatform) location
123
+ DI_API_ENDPOINT=URL - to point to a non-standard Build (DataIntegration) location
124
124
  OAUTH_TOKEN_URI=URL - to point to an external IdentityProvider location
125
125
  OAUTH_USER=username - only if OAUTH_GRANT_TYPE=password
126
126
  OAUTH_PASSWORD=password - only if OAUTH_GRANT_TYPE=password
@@ -14,41 +14,34 @@ from xml.etree.ElementTree import ( # nosec
14
14
  )
15
15
 
16
16
  import click
17
- from click import Argument, ClickException, UsageError
17
+ from click import ClickException, Context, UsageError
18
18
  from cmem.cmempy.config import get_cmem_base_uri
19
19
  from cmem.cmempy.dp.authorization import refresh
20
20
  from cmem.cmempy.dp.proxy import graph as graph_api
21
- from cmem.cmempy.dp.proxy.graph import get_graph_import_tree, get_graph_imports
21
+ from cmem.cmempy.dp.proxy.graph import get_graph_imports
22
22
  from cmem.cmempy.dp.proxy.sparql import get as sparql_api
23
23
  from jinja2 import Template
24
24
  from six.moves.urllib.parse import quote
25
- from treelib import Tree
26
25
 
27
26
  from cmem_cmemc import completion
28
27
  from cmem_cmemc.command import CmemcCommand
29
28
  from cmem_cmemc.command_group import CmemcGroup
29
+ from cmem_cmemc.commands.graph_imports import graph_imports_list, imports_group
30
30
  from cmem_cmemc.commands.validation import validation_group
31
+ from cmem_cmemc.constants import UNKNOWN_GRAPH_ERROR
31
32
  from cmem_cmemc.context import ApplicationContext
32
33
  from cmem_cmemc.parameter_types.path import ClickSmartPath
33
34
  from cmem_cmemc.smart_path import SmartPath
34
35
  from cmem_cmemc.utils import (
36
+ RdfGraphData,
35
37
  convert_uri_to_filename,
36
38
  get_graphs,
37
39
  get_graphs_as_dict,
38
40
  iri_to_qname,
39
41
  read_rdf_graph_files,
42
+ tuple_to_list,
40
43
  )
41
44
 
42
- UNKNOWN_GRAPH_ERROR = "The graph {} is not accessible or does not exist."
43
-
44
-
45
- def tuple_to_list(ctx: ApplicationContext, param: Argument, value: tuple) -> list: # noqa: ARG001
46
- """Get a list from a tuple
47
-
48
- Used as callback to have mutable values
49
- """
50
- return list(value)
51
-
52
45
 
53
46
  def count_graph(graph_iri: str) -> int:
54
47
  """Count triples in a graph and return integer."""
@@ -142,63 +135,6 @@ def _get_export_names(
142
135
  return _names
143
136
 
144
137
 
145
- def _create_node_label(iri: str, graphs: dict) -> str:
146
- """Create a label for a node in the tree."""
147
- if iri not in graphs:
148
- return "[missing: " + iri + "]"
149
- title = graphs[iri]["label"]["title"]
150
- return f"{title} -- {iri}"
151
-
152
-
153
- def _add_tree_nodes_recursive(tree: Tree, structure: dict, iri: str, graphs: dict) -> Tree:
154
- """Add all child nodes of iri from structure to tree.
155
-
156
- Call recursively until no child node can be used as parent anymore.
157
-
158
- Args:
159
- ----
160
- tree: the graph where to add the nodes
161
- structure: the result dict of get_graph_import_tree()
162
- iri: The IRI of the parent
163
- graphs: the result of get_graphs()
164
-
165
- Returns:
166
- -------
167
- the new treelib.Tree object with the additional nodes
168
-
169
- """
170
- if not tree.contains(iri):
171
- tree.create_node(tag=_create_node_label(iri, graphs), identifier=iri)
172
- if iri not in structure:
173
- return tree
174
- for child in structure[iri]:
175
- tree.create_node(tag=_create_node_label(child, graphs), identifier=child, parent=iri)
176
- for child in structure[iri]:
177
- if child in structure:
178
- tree = _add_tree_nodes_recursive(tree, structure, child, graphs)
179
- return tree
180
-
181
-
182
- def _add_ignored_nodes(tree: Tree, structure: dict) -> Tree:
183
- """Add all child nodes as ignored nodes.
184
-
185
- Args:
186
- ----
187
- tree: the graph where to add the nodes
188
- structure: the result dict of get_graph_import_tree()
189
-
190
- Returns:
191
- -------
192
- the new treelib.Tree object with the additional nodes
193
-
194
- """
195
- if len(structure.keys()) > 0:
196
- for parent in structure:
197
- for children in structure[parent]:
198
- tree.create_node(tag="[ignored: " + children + "]", parent=parent)
199
- return tree
200
-
201
-
202
138
  def _get_graphs_filtered(filter_name: str, filter_value: str) -> list[dict]:
203
139
  """Get graphs but filtered according to name and value."""
204
140
  # not filtered means all graphs
@@ -322,29 +258,7 @@ def _create_xml_catalog_file(app: ApplicationContext, names: dict, output_dir: s
322
258
  app.echo_success(f"XML catalog file created as {file_name}.")
323
259
 
324
260
 
325
- def _prepare_tree_output_id_only(iris: list[str], graphs: dict) -> str:
326
- """Prepare a sorted, de-duplicated IRI list of graph imports."""
327
- output_iris = []
328
- for iri in iris:
329
- # get response for one requested graph
330
- api_response = get_graph_import_tree(iri)
331
-
332
- # add all imported IRIs to the IRI list
333
- # add the requested graph as well
334
- output_iris.append(iri)
335
- for top_graph in api_response["tree"]:
336
- output_iris.append(top_graph)
337
- for sub_graph in api_response["tree"][top_graph]:
338
- output_iris.append(sub_graph) # noqa: PERF402
339
-
340
- # prepare a sorted, de-duplicated IRI list of existing graphs
341
- # and create a line-by-line output of it
342
- output_iris = sorted(set(output_iris), key=lambda x: x.lower())
343
- filtered_iris = [iri for iri in output_iris if iri in graphs]
344
- return "\n".join(filtered_iris[0:]) + "\n"
345
-
346
-
347
- @click.command(cls=CmemcCommand, name="tree")
261
+ @click.command(cls=CmemcCommand, name="tree", hidden=True)
348
262
  @click.option("-a", "--all", "all_", is_flag=True, help="Show tree of all (readable) graphs.")
349
263
  @click.option("--raw", is_flag=True, help="Outputs raw JSON of the graph importTree API response.")
350
264
  @click.option(
@@ -362,64 +276,17 @@ def _prepare_tree_output_id_only(iris: list[str], graphs: dict) -> str:
362
276
  shell_complete=completion.graph_uris,
363
277
  callback=tuple_to_list,
364
278
  )
365
- @click.pass_obj
366
- def tree_command(
367
- app: ApplicationContext, all_: bool, raw: bool, id_only: bool, iris: list[str]
368
- ) -> None:
369
- """Show graph tree(s) of the owl:imports hierarchy.
370
-
371
- You can output one or more trees of the import hierarchy.
372
-
373
- Imported graphs which do not exist are shown as `[missing: IRI]`.
374
- Imported graphs which will result in an import cycle are shown as
375
- `[ignored: IRI]`.
376
- Each graph is shown with label and IRI.
377
- """
378
- graphs = get_graphs_as_dict()
379
- if not iris and not all_:
380
- raise UsageError(
381
- "Either specify at least one graph IRI or use the "
382
- "--all option to show the owl:imports tree of all graphs."
383
- )
384
- if all_:
385
- iris = [str(_) for _ in graphs]
386
-
387
- for iri in iris:
388
- if iri not in graphs:
389
- raise ClickException(UNKNOWN_GRAPH_ERROR.format(iri))
390
-
391
- iris = sorted(iris, key=lambda x: graphs[x]["label"]["title"].lower())
392
-
393
- if raw:
394
- for iri in iris:
395
- # direct output of the response for one requested graph
396
- app.echo_info_json(get_graph_import_tree(iri))
397
- return
398
-
399
- if id_only:
400
- app.echo_result(_prepare_tree_output_id_only(iris, graphs), nl=False)
401
- return
402
-
403
- # normal execution
404
- output = ""
405
- for iri in iris:
406
- # get response for on requested graph
407
- api_response = get_graph_import_tree(iri)
408
-
409
- tree = _add_tree_nodes_recursive(Tree(), api_response["tree"], iri, graphs)
410
- tree = _add_ignored_nodes(tree, api_response["ignored"])
411
-
412
- # strip empty lines from the tree.show output
413
- output += os.linesep.join(
414
- [
415
- line
416
- for line in tree.show(key=lambda x: x.tag.lower(), stdout=False).splitlines() # type: ignore[arg-type, return-value]
417
- if line.strip()
418
- ]
419
- )
420
- output += "\n"
421
- # result output
422
- app.echo_result(output, nl=False)
279
+ @click.pass_context
280
+ def tree_command(ctx: Context, all_: bool, raw: bool, id_only: bool, iris: list[str]) -> None:
281
+ """(Hidden) Deprecated: use 'graph imports tree' instead."""
282
+ app: ApplicationContext = ctx.obj
283
+ app.echo_warning(
284
+ "The 'tree' command is deprecated and will be removed with the next major release."
285
+ " Please use the 'graph imports tree' command instead."
286
+ )
287
+ imports_cmd = graph.commands["imports"]
288
+ tree_cmd = imports_cmd.commands["tree"] # type: ignore[attr-defined]
289
+ ctx.invoke(tree_cmd, all_=all_, raw=raw, id_only=id_only, iris=iris)
423
290
 
424
291
 
425
292
  @click.command(cls=CmemcCommand, name="list")
@@ -494,6 +361,15 @@ def _validate_export_command_input_parameters(
494
361
  raise UsageError("Cannot output a binary file to terminal. Use --output-file option.")
495
362
 
496
363
 
364
+ def _write_graph_imports(ctx: Context, filename: str, iri: str) -> None:
365
+ imports = graph_imports_list.apply_filters(ctx=ctx, filter_=[("to-graph", iri)])
366
+ if imports:
367
+ imports_file = click.open_file(filename, "w")
368
+ for _ in imports:
369
+ imports_file.write(_["from_graph"] + "\n")
370
+ imports_file.close()
371
+
372
+
497
373
  # pylint: disable=too-many-arguments,too-many-locals
498
374
  @click.command(cls=CmemcCommand, name="export")
499
375
  @click.option("-a", "--all", "all_", is_flag=True, help="Export all readable graphs.")
@@ -503,6 +379,12 @@ def _validate_export_command_input_parameters(
503
379
  help="Export selected graph(s) and all graphs which are imported from "
504
380
  "these selected graph(s).",
505
381
  )
382
+ @click.option(
383
+ "--include-import-statements",
384
+ is_flag=True,
385
+ help="Save graph imports information from other graphs to the exported graphs "
386
+ "and write *.imports files.",
387
+ )
506
388
  @click.option(
507
389
  "--create-catalog",
508
390
  is_flag=True,
@@ -560,11 +442,12 @@ def _validate_export_command_input_parameters(
560
442
  shell_complete=completion.graph_uris,
561
443
  callback=tuple_to_list,
562
444
  )
563
- @click.pass_obj
564
- def export_command( # noqa: PLR0913
565
- app: ApplicationContext,
445
+ @click.pass_context
446
+ def export_command( # noqa: C901, PLR0913
447
+ ctx: Context,
566
448
  all_: bool,
567
449
  include_imports: bool,
450
+ include_import_statements: bool,
568
451
  create_catalog: bool,
569
452
  output_dir: str,
570
453
  output_file: str,
@@ -580,6 +463,7 @@ def export_command( # noqa: PLR0913
580
463
  In case of directory export, .graph and .ttl files will be created
581
464
  for each graph.
582
465
  """
466
+ app: ApplicationContext = ctx.obj
583
467
  _validate_export_command_input_parameters(output_dir, output_file, compress, create_catalog)
584
468
  iris = _check_and_extend_exported_graphs(iris, all_, include_imports, get_graphs_as_dict())
585
469
 
@@ -595,6 +479,8 @@ def export_command( # noqa: PLR0913
595
479
  app, iris, template, f"{extension}.gz" if compress else f"{extension}"
596
480
  )
597
481
  _graph_file_names = _get_export_names(app, iris, template, f"{extension}.graph")
482
+ _graph_imports_file_names = _get_export_names(app, iris, template, f"{extension}.imports")
483
+
598
484
  # create directory
599
485
  if not SmartPath(output_dir).exists():
600
486
  app.echo_warning("Output directory does not exist: " + "will create it.")
@@ -604,14 +490,21 @@ def export_command( # noqa: PLR0913
604
490
  # join with given output directory and normalize full path
605
491
  triple_file_name = os.path.normpath(SmartPath(output_dir) / _names[iri])
606
492
  graph_file_name = os.path.normpath(SmartPath(output_dir) / _graph_file_names[iri])
493
+ imports_file_name = os.path.normpath(
494
+ SmartPath(output_dir) / _graph_imports_file_names[iri]
495
+ )
607
496
  # output directory is created lazy
608
497
  SmartPath(triple_file_name).parent.mkdir(parents=True, exist_ok=True)
609
498
  # create and write the .ttl.graph metadata file
610
499
  graph_file = click.open_file(graph_file_name, "w")
611
500
  graph_file.write(iri + "\n")
501
+ graph_file.close()
502
+ if include_import_statements:
503
+ _write_graph_imports(ctx=ctx, filename=imports_file_name, iri=iri)
612
504
  _get_graph_to_file(
613
505
  iri, triple_file_name, app, numbers=(current, count), mime_type=mime_type
614
506
  )
507
+
615
508
  if create_catalog:
616
509
  _create_xml_catalog_file(app, _names, output_dir)
617
510
  return
@@ -706,6 +599,64 @@ def _get_buffer_and_content_type(
706
599
  return buffer, content_type
707
600
 
708
601
 
602
+ def _create_graph_imports(ctx: Context, graphs: list[RdfGraphData]) -> None:
603
+ # Locate and invoke the 'create' subcommand under 'graph imports' command
604
+ app: ApplicationContext = ctx.obj
605
+ imports_cmd = graph.commands["imports"]
606
+ create_cmd = imports_cmd.commands["create"] # type: ignore[attr-defined]
607
+ for _ in graphs:
608
+ if not _.graph_imports:
609
+ continue
610
+ for _import in _.graph_imports:
611
+ from_graph = _import
612
+ to_graph = _.graph_iri
613
+ imports = graph_imports_list.apply_filters(
614
+ ctx=ctx, filter_=[("from-graph", from_graph), ("to-graph", to_graph)]
615
+ )
616
+ if imports:
617
+ app.echo_info(
618
+ f"Creating graph import from {from_graph} to {to_graph} ... ", nl=False
619
+ )
620
+ app.echo_warning("exists")
621
+ else:
622
+ ctx.invoke(create_cmd, from_graph=from_graph, to_graph=to_graph)
623
+
624
+
625
+ def _process_input_directory(input_path: str, iri: str) -> list[RdfGraphData]:
626
+ if iri is None:
627
+ # in case a directory is the source (and no IRI is given),
628
+ # the graph/nt file structure is crawled
629
+ graphs = read_rdf_graph_files(input_path)
630
+ else:
631
+ # in case a directory is the source AND IRI is given
632
+ graphs = []
633
+ for _ in _get_graph_supported_formats():
634
+ extension = mimetypes.guess_extension(_)
635
+ graphs += [
636
+ RdfGraphData(str(file), iri, [])
637
+ for file in SmartPath(input_path).glob(f"*{extension}")
638
+ ]
639
+ graphs += [
640
+ RdfGraphData(str(file), iri, [])
641
+ for file in SmartPath(input_path).glob(f"*{extension}.gz")
642
+ ]
643
+ return graphs
644
+
645
+
646
+ def _validate_graph_imports(graphs_to_import: list[RdfGraphData]) -> None:
647
+ graphs = {_["iri"] for _ in get_graphs()}
648
+ graphs.update({_.graph_iri for _ in graphs_to_import})
649
+ graph_imports: set[str] = set()
650
+ for graph_data in graphs_to_import:
651
+ for item in graph_data.graph_imports:
652
+ graph_imports.add(str(item))
653
+ if graph_imports - graphs:
654
+ raise click.UsageError(
655
+ f"The following graphs are not available, "
656
+ f"so you can not add import statements to them: {','.join(graph_imports - graphs)}"
657
+ )
658
+
659
+
709
660
  @click.command(cls=CmemcCommand, name="import")
710
661
  @click.option(
711
662
  "--replace",
@@ -720,6 +671,11 @@ def _get_buffer_and_content_type(
720
671
  "beginning of the process, so that you can still add multiple "
721
672
  "files to one single graph (if it does not exist).",
722
673
  )
674
+ @click.option(
675
+ "--include-import-statements",
676
+ is_flag=True,
677
+ help="Use *.imports files to re-apply the graph imports of the imported graphs.",
678
+ )
723
679
  @click.argument(
724
680
  "input_path",
725
681
  required=True,
@@ -727,12 +683,13 @@ def _get_buffer_and_content_type(
727
683
  type=ClickSmartPath(allow_dash=False, readable=True, remote_okay=True),
728
684
  )
729
685
  @click.argument("iri", type=click.STRING, required=False, shell_complete=completion.graph_uris)
730
- @click.pass_obj
731
- def import_command(
732
- app: ApplicationContext,
686
+ @click.pass_context
687
+ def import_command( # noqa: PLR0913
688
+ ctx: Context,
733
689
  input_path: str,
734
690
  replace: bool,
735
691
  skip_existing: bool,
692
+ include_import_statements: bool,
736
693
  iri: str,
737
694
  ) -> None:
738
695
  """Import graph(s) to the store.
@@ -752,35 +709,25 @@ def import_command(
752
709
 
753
710
  Note: Directories are scanned on the first level only (not recursively).
754
711
  """
712
+ app: ApplicationContext = ctx.obj
755
713
  if replace and skip_existing:
756
714
  raise UsageError(
757
715
  "The options --replace and --skip-existing are mutually "
758
716
  "exclusive, so please remove one of them."
759
717
  )
760
718
  # is an array of tuples like this [('path/to/triple.file', 'graph IRI')]
761
- graphs: list[tuple[str, str]]
719
+ graphs: list[RdfGraphData]
762
720
  if SmartPath(input_path).is_dir():
763
721
  validate_input_path(input_path)
764
- if iri is None:
765
- # in case a directory is the source (and no IRI is given),
766
- # the graph/nt file structure is crawled
767
- graphs = read_rdf_graph_files(input_path)
768
- else:
769
- # in case a directory is the source AND IRI is given
770
- graphs = []
771
- for _ in _get_graph_supported_formats():
772
- extension = mimetypes.guess_extension(_)
773
- graphs += [(str(file), iri) for file in SmartPath(input_path).glob(f"*{extension}")]
774
- graphs += [
775
- (str(file), iri) for file in SmartPath(input_path).glob(f"*{extension}.gz")
776
- ]
777
-
722
+ graphs = _process_input_directory(input_path, iri)
723
+ if include_import_statements:
724
+ _validate_graph_imports(graphs)
778
725
  elif SmartPath(input_path).is_file():
779
726
  if iri is None:
780
727
  raise UsageError(
781
728
  "Either specify an input file AND a graph IRI or an input directory ONLY."
782
729
  )
783
- graphs = [(input_path, iri)]
730
+ graphs = [RdfGraphData(input_path, iri, [])]
784
731
  else:
785
732
  raise NotImplementedError(
786
733
  "Input from special files (socket, FIFO, device file) is not supported."
@@ -790,7 +737,9 @@ def import_command(
790
737
  processed_graphs: set = set()
791
738
  count: int = len(graphs)
792
739
  current: int = 1
793
- for triple_file, graph_iri in graphs:
740
+ for _ in graphs:
741
+ triple_file = _.file_path
742
+ graph_iri = _.graph_iri
794
743
  app.echo_info(
795
744
  f"Import file {current}/{count}: " f"{graph_iri} from {triple_file} ... ", nl=False
796
745
  )
@@ -814,6 +763,9 @@ def import_command(
814
763
  app.echo_debug("Access conditions refreshed.")
815
764
  processed_graphs.add(graph_iri)
816
765
  current += 1
766
+ # create graph imports
767
+ if include_import_statements:
768
+ _create_graph_imports(ctx, graphs)
817
769
 
818
770
 
819
771
  @click.command(cls=CmemcCommand, name="delete")
@@ -824,6 +776,9 @@ def import_command(
824
776
  help="Delete selected graph(s) and all writeable graphs which are "
825
777
  "imported from these selected graph(s).",
826
778
  )
779
+ @click.option(
780
+ "--include-import-statements", is_flag=True, help="Delete import reference of deleted graphs"
781
+ )
827
782
  @click.argument(
828
783
  "iris",
829
784
  nargs=-1,
@@ -831,16 +786,25 @@ def import_command(
831
786
  shell_complete=completion.writable_graph_uris,
832
787
  callback=tuple_to_list,
833
788
  )
834
- @click.pass_obj
789
+ @click.pass_context
835
790
  def delete_command(
836
- app: ApplicationContext, all_: bool, include_imports: bool, iris: list[str]
791
+ ctx: Context,
792
+ all_: bool,
793
+ include_imports: bool,
794
+ include_import_statements: bool,
795
+ iris: list[str],
837
796
  ) -> None:
838
797
  """Delete graph(s) from the store."""
798
+ app: ApplicationContext = ctx.obj
839
799
  graphs = get_graphs_as_dict(writeable=True, readonly=False)
840
800
  iris = _check_and_extend_exported_graphs(iris, all_, include_imports, graphs)
841
-
801
+ imports_to_be_deleted = []
842
802
  count: int = len(iris)
843
803
  for current, iri in enumerate(iris, start=1):
804
+ imports_to_be_deleted += graph_imports_list.apply_filters(
805
+ ctx=ctx, filter_=[("to-graph", iri)]
806
+ )
807
+
844
808
  app.echo_info(f"Drop graph {current}/{count}: {iri} ... ", nl=False)
845
809
  graph_api.delete(iri)
846
810
  app.echo_success("done")
@@ -848,6 +812,13 @@ def delete_command(
848
812
  if iri == refresh.AUTHORIZATION_GRAPH_URI:
849
813
  refresh.get()
850
814
  app.echo_debug("Access conditions refreshed.")
815
+ if include_import_statements:
816
+ # Locate and invoke the 'delete' subcommand under 'graph imports' command
817
+ imports_cmd = graph.commands["imports"]
818
+ delete_cmd = imports_cmd.commands["delete"] # type: ignore[attr-defined]
819
+ for _ in imports_to_be_deleted:
820
+ if _["from_graph"] not in iris:
821
+ ctx.invoke(delete_cmd, from_graph=_["from_graph"], to_graph=_["to_graph"])
851
822
 
852
823
 
853
824
  @click.command(cls=CmemcCommand, name="open")
@@ -914,3 +885,4 @@ graph.add_command(delete_command)
914
885
  graph.add_command(import_command)
915
886
  graph.add_command(open_command)
916
887
  graph.add_command(validation_group)
888
+ graph.add_command(imports_group)