cmem-cmemc 25.1.1__py3-none-any.whl → 25.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,420 @@
1
+ """Graph imports command"""
2
+
3
+ import os
4
+
5
+ import click
6
+ from click import Argument, ClickException, Context, UsageError
7
+ from click.shell_completion import CompletionItem
8
+ from cmem.cmempy.dp.proxy.graph import get_graph_import_tree
9
+ from cmem.cmempy.queries import SparqlQuery
10
+ from treelib import Tree
11
+
12
+ from cmem_cmemc import completion
13
+ from cmem_cmemc.command import CmemcCommand
14
+ from cmem_cmemc.command_group import CmemcGroup
15
+ from cmem_cmemc.completion import escape_colon
16
+ from cmem_cmemc.constants import UNKNOWN_GRAPH_ERROR
17
+ from cmem_cmemc.context import ApplicationContext
18
+ from cmem_cmemc.object_list import DirectValuePropertyFilter, ObjectList
19
+ from cmem_cmemc.string_processor import GraphLink
20
+ from cmem_cmemc.title_helper import TitleHelper
21
+ from cmem_cmemc.utils import get_graphs_as_dict, tuple_to_list
22
+
23
+ GRAPH_IMPORTS_LIST_SPARQL = """
24
+ PREFIX owl: <http://www.w3.org/2002/07/owl#>
25
+
26
+ SELECT ?from_graph ?to_graph
27
+ WHERE
28
+ {
29
+ GRAPH ?from_graph {
30
+ ?from_graph owl:imports ?to_graph
31
+ }
32
+ }
33
+ """
34
+
35
+ GRAPH_IMPORTS_CREATE_SPARQL = """
36
+ PREFIX owl: <http://www.w3.org/2002/07/owl#>
37
+
38
+ INSERT DATA {
39
+ GRAPH <{{from_graph}}> {
40
+ <{{from_graph}}> owl:imports <{{to_graph}}> .
41
+ }
42
+ }
43
+ """
44
+
45
+ GRAPH_IMPORTS_DELETE_SPARQL = """
46
+ PREFIX owl: <http://www.w3.org/2002/07/owl#>
47
+
48
+ DELETE DATA {
49
+ GRAPH <{{from_graph}}> {
50
+ <{{from_graph}}> owl:imports <{{to_graph}}> .
51
+ }
52
+ }
53
+ """
54
+
55
+
56
+ def _prepare_tree_output_id_only(iris: list[str], graphs: dict) -> str:
57
+ """Prepare a sorted, de-duplicated IRI list of graph imports."""
58
+ output_iris = []
59
+ for iri in iris:
60
+ # get response for one requested graph
61
+ api_response = get_graph_import_tree(iri)
62
+
63
+ # add all imported IRIs to the IRI list
64
+ # add the requested graph as well
65
+ output_iris.append(iri)
66
+ for top_graph in api_response["tree"]:
67
+ output_iris.append(top_graph)
68
+ for sub_graph in api_response["tree"][top_graph]:
69
+ output_iris.append(sub_graph) # noqa: PERF402
70
+
71
+ # prepare a sorted, de-duplicated IRI list of existing graphs
72
+ # and create a line-by-line output of it
73
+ output_iris = sorted(set(output_iris), key=lambda x: x.lower())
74
+ filtered_iris = [iri for iri in output_iris if iri in graphs]
75
+ return "\n".join(filtered_iris[0:]) + "\n"
76
+
77
+
78
+ def _create_node_label(iri: str, graphs: dict) -> str:
79
+ """Create a label for a node in the tree."""
80
+ if iri not in graphs:
81
+ return "[missing: " + iri + "]"
82
+ title = graphs[iri]["label"]["title"]
83
+ return f"{title} -- {iri}"
84
+
85
+
86
+ def _add_tree_nodes_recursive(tree: Tree, structure: dict, iri: str, graphs: dict) -> Tree:
87
+ """Add all child nodes of iri from structure to tree.
88
+
89
+ Call recursively until no child node can be used as parent anymore.
90
+
91
+ Args:
92
+ ----
93
+ tree: the graph where to add the nodes
94
+ structure: the result dict of get_graph_import_tree()
95
+ iri: The IRI of the parent
96
+ graphs: the result of get_graphs()
97
+
98
+ Returns:
99
+ -------
100
+ the new treelib.Tree object with the additional nodes
101
+
102
+ """
103
+ if not tree.contains(iri):
104
+ tree.create_node(tag=_create_node_label(iri, graphs), identifier=iri)
105
+ if iri not in structure:
106
+ return tree
107
+ for child in structure[iri]:
108
+ tree.create_node(tag=_create_node_label(child, graphs), identifier=child, parent=iri)
109
+ for child in structure[iri]:
110
+ if child in structure:
111
+ tree = _add_tree_nodes_recursive(tree, structure, child, graphs)
112
+ return tree
113
+
114
+
115
+ def _add_ignored_nodes(tree: Tree, structure: dict) -> Tree:
116
+ """Add all child nodes as ignored nodes.
117
+
118
+ Args:
119
+ ----
120
+ tree: the graph where to add the nodes
121
+ structure: the result dict of get_graph_import_tree()
122
+
123
+ Returns:
124
+ -------
125
+ the new treelib.Tree object with the additional nodes
126
+
127
+ """
128
+ if len(structure.keys()) > 0:
129
+ for parent in structure:
130
+ for children in structure[parent]:
131
+ tree.create_node(tag="[ignored: " + children + "]", parent=parent)
132
+ return tree
133
+
134
+
135
+ def get_imports_list(ctx: click.Context) -> list[dict[str, str]]: # noqa: ARG001
136
+ """Get the import list"""
137
+ list_query = SparqlQuery(text=GRAPH_IMPORTS_LIST_SPARQL)
138
+ result = list_query.get_json_results()
139
+ return [
140
+ {"from_graph": _["from_graph"]["value"], "to_graph": _["to_graph"]["value"]}
141
+ for _ in result["results"]["bindings"]
142
+ ]
143
+
144
+
145
+ @click.command(cls=CmemcCommand, name="tree")
146
+ @click.option("-a", "--all", "all_", is_flag=True, help="Show tree of all (readable) graphs.")
147
+ @click.option("--raw", is_flag=True, help="Outputs raw JSON of the graph importTree API response.")
148
+ @click.option(
149
+ "--id-only",
150
+ is_flag=True,
151
+ help="Lists only graph identifier (IRIs) and no labels or other "
152
+ "metadata. This is useful for piping the IRIs into other commands. "
153
+ "The output with this option is a sorted, flat, de-duplicated list "
154
+ "of existing graphs.",
155
+ )
156
+ @click.argument(
157
+ "iris",
158
+ nargs=-1,
159
+ type=click.STRING,
160
+ shell_complete=completion.graph_uris,
161
+ callback=tuple_to_list,
162
+ )
163
+ @click.pass_obj
164
+ def tree_command(
165
+ app: ApplicationContext, all_: bool, raw: bool, id_only: bool, iris: list[str]
166
+ ) -> None:
167
+ """Show graph tree(s) of the imports statement hierarchy.
168
+
169
+ You can output one or more trees of the import hierarchy.
170
+
171
+ Imported graphs which do not exist are shown as `[missing: IRI]`.
172
+ Imported graphs which will result in an import cycle are shown as
173
+ `[ignored: IRI]`.
174
+ Each graph is shown with label and IRI.
175
+ """
176
+ graphs = get_graphs_as_dict()
177
+ if not iris and not all_:
178
+ raise UsageError(
179
+ "Either specify at least one graph IRI or use the "
180
+ "--all option to show the owl:imports tree of all graphs."
181
+ )
182
+ if all_:
183
+ iris = [str(_) for _ in graphs]
184
+
185
+ for iri in iris:
186
+ if iri not in graphs:
187
+ raise ClickException(UNKNOWN_GRAPH_ERROR.format(iri))
188
+
189
+ iris = sorted(iris, key=lambda x: graphs[x]["label"]["title"].lower())
190
+
191
+ if raw:
192
+ for iri in iris:
193
+ # direct output of the response for one requested graph
194
+ app.echo_info_json(get_graph_import_tree(iri))
195
+ return
196
+
197
+ if id_only:
198
+ app.echo_result(_prepare_tree_output_id_only(iris, graphs), nl=False)
199
+ return
200
+
201
+ # normal execution
202
+ output = ""
203
+ for iri in iris:
204
+ # get response for on requested graph
205
+ api_response = get_graph_import_tree(iri)
206
+
207
+ tree = _add_tree_nodes_recursive(Tree(), api_response["tree"], iri, graphs)
208
+ tree = _add_ignored_nodes(tree, api_response["ignored"])
209
+
210
+ # strip empty lines from the tree.show output
211
+ output += os.linesep.join(
212
+ [
213
+ line
214
+ for line in tree.show(key=lambda x: x.tag.lower(), stdout=False).splitlines() # type: ignore[arg-type, return-value]
215
+ if line.strip()
216
+ ]
217
+ )
218
+ output += "\n"
219
+ # result output
220
+ app.echo_result(output, nl=False)
221
+
222
+
223
+ graph_imports_list = ObjectList(
224
+ name="imports",
225
+ get_objects=get_imports_list,
226
+ filters=[
227
+ DirectValuePropertyFilter(
228
+ name="from-graph",
229
+ description="List only matches from graph",
230
+ property_key="from_graph",
231
+ title_helper=TitleHelper(),
232
+ ),
233
+ DirectValuePropertyFilter(
234
+ name="to-graph",
235
+ description="List only matches to graph",
236
+ property_key="to_graph",
237
+ title_helper=TitleHelper(),
238
+ ),
239
+ ],
240
+ )
241
+
242
+
243
+ @click.command(cls=CmemcCommand, name="list")
244
+ @click.option("--raw", is_flag=True, help="Outputs raw JSON response.")
245
+ @click.option(
246
+ "--filter",
247
+ "filter_",
248
+ type=(str, str),
249
+ help=graph_imports_list.get_filter_help_text(),
250
+ shell_complete=graph_imports_list.complete_values,
251
+ )
252
+ @click.pass_context
253
+ def list_command(ctx: Context, raw: bool, filter_: tuple[str, str]) -> None:
254
+ """List accessible graph imports statements.
255
+
256
+ Graphs are identified by an IRI. Statement imports are managed by
257
+ creating owl:imports statements such as "FROM_GRAPH owl:imports TO_GRAPH"
258
+ in the FROM_GRAPH. All statements in the TO_GRAPH are then available
259
+ in the FROM_GRAPH.
260
+ """
261
+ app: ApplicationContext = ctx.obj
262
+ filters_to_apply = []
263
+ if filter_:
264
+ filters_to_apply.append(filter_)
265
+ imports = graph_imports_list.apply_filters(ctx=ctx, filter_=filters_to_apply)
266
+
267
+ if raw:
268
+ app.echo_info_json(imports)
269
+ return
270
+
271
+ table = []
272
+ graphs = get_graphs_as_dict()
273
+ for _ in imports:
274
+ from_graph = _["from_graph"]
275
+ to_graph = _["to_graph"]
276
+ if to_graph not in graphs:
277
+ to_graph = rf"\[missing: {to_graph}]"
278
+ table.append([from_graph, to_graph])
279
+
280
+ app.echo_info_table(
281
+ table,
282
+ headers=["From graph", "To graph"],
283
+ sort_column=0,
284
+ cell_processing={0: GraphLink(), 1: GraphLink()},
285
+ empty_table_message="No imports found. "
286
+ "You can use the `graph imports create` command to create a graph import.",
287
+ )
288
+
289
+
290
+ def _validate_graphs(from_graph: str | None, to_graph: str | None) -> None:
291
+ graphs = get_graphs_as_dict(writeable=True, readonly=True)
292
+ if from_graph and from_graph not in graphs:
293
+ raise click.UsageError(f"From graph {from_graph} not found.")
294
+
295
+ if to_graph and to_graph not in graphs:
296
+ raise click.UsageError(f"To graph {to_graph} not found.")
297
+
298
+
299
+ def _from_graph_uris(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]:
300
+ """Provide auto completion items for delete command from-graph argument"""
301
+ imports = get_imports_list(ctx)
302
+ from_graphs = {escape_colon(_["from_graph"]) for _ in imports}
303
+ return [
304
+ _
305
+ for _ in completion.graph_uris(ctx=ctx, param=param, incomplete=incomplete)
306
+ if _.value in from_graphs
307
+ ]
308
+
309
+
310
+ def _to_graph_uris(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]:
311
+ """Provide auto completion items for create/delete command to-graph argument"""
312
+ from_graph = ctx.params["from_graph"]
313
+ imports = graph_imports_list.apply_filters(ctx=ctx, filter_=[("from-graph", from_graph)])
314
+ to_graphs = {escape_colon(_["to_graph"]) for _ in imports}
315
+ command = ctx.command.name
316
+ return [
317
+ _
318
+ for _ in completion.graph_uris(ctx=ctx, param=param, incomplete=incomplete)
319
+ if (command == "delete" and _.value in to_graphs)
320
+ or (
321
+ command == "create" and _.value not in to_graphs and _.value != escape_colon(from_graph)
322
+ )
323
+ ]
324
+
325
+
326
+ @click.command(cls=CmemcCommand, name="create")
327
+ @click.argument("from_graph", type=str, shell_complete=completion.graph_uris)
328
+ @click.argument("to_graph", type=str, shell_complete=_to_graph_uris)
329
+ @click.pass_context
330
+ def create_command(ctx: Context, from_graph: str, to_graph: str) -> None:
331
+ """Add statement to import a TO_GRAPH into a FROM_GRAPH.
332
+
333
+ Graphs are identified by an IRI. Statement imports are managed by
334
+ creating owl:imports statements such as "FROM_GRAPH owl:imports TO_GRAPH"
335
+ in the FROM_GRAPH. All statements in the TO_GRAPH are then available
336
+ in the FROM_GRAPH.
337
+
338
+ Note: The get a list of existing graphs, execute the `graph list` command or
339
+ use tab-completion.
340
+ """
341
+ app: ApplicationContext = ctx.obj
342
+ _validate_graphs(from_graph, to_graph)
343
+ if from_graph == to_graph:
344
+ raise click.UsageError("From graph and to graph cannot be the same.")
345
+
346
+ imports = graph_imports_list.apply_filters(
347
+ ctx=ctx, filter_=[("from-graph", from_graph), ("to-graph", to_graph)]
348
+ )
349
+ if imports:
350
+ raise click.UsageError("Import combination already exists.")
351
+ app.echo_info(f"Creating graph import from {from_graph} to {to_graph} ... ", nl=False)
352
+
353
+ create_query = SparqlQuery(
354
+ text=GRAPH_IMPORTS_CREATE_SPARQL,
355
+ query_type="UPDATE",
356
+ )
357
+ create_query.get_results(
358
+ placeholder={
359
+ "from_graph": from_graph,
360
+ "to_graph": to_graph,
361
+ }
362
+ )
363
+ app.echo_success("done")
364
+
365
+
366
+ @click.command(cls=CmemcCommand, name="delete")
367
+ @click.argument("from_graph", type=str, shell_complete=_from_graph_uris)
368
+ @click.argument("to_graph", type=str, shell_complete=_to_graph_uris)
369
+ @click.pass_context
370
+ def delete_command(ctx: Context, from_graph: str, to_graph: str) -> None:
371
+ """Delete statement to import a TO_GRAPH into a FROM_GRAPH.
372
+
373
+ Graphs are identified by an IRI. Statement imports are managed by
374
+ creating owl:imports statements such as "FROM_GRAPH owl:imports TO_GRAPH"
375
+ in the FROM_GRAPH. All statements in the TO_GRAPH are then available
376
+ in the FROM_GRAPH.
377
+
378
+ Note: The get a list of existing graph imports, execute the
379
+ `graph imports list` command or use tab-completion.
380
+ """
381
+ app: ApplicationContext = ctx.obj
382
+ _validate_graphs(from_graph, None)
383
+ imports = graph_imports_list.apply_filters(
384
+ ctx=ctx, filter_=[("from-graph", from_graph), ("to-graph", to_graph)]
385
+ )
386
+ if not imports:
387
+ raise click.UsageError("Import combination does not exists.")
388
+ app.echo_info(f"Deleting graph import from {from_graph} to {to_graph} ... ", nl=False)
389
+
390
+ delete_query = SparqlQuery(
391
+ text=GRAPH_IMPORTS_DELETE_SPARQL,
392
+ query_type="UPDATE",
393
+ )
394
+ delete_query.get_results(
395
+ placeholder={
396
+ "from_graph": from_graph,
397
+ "to_graph": to_graph,
398
+ }
399
+ )
400
+ app.echo_success("done")
401
+
402
+
403
+ @click.group(cls=CmemcGroup, name="imports")
404
+ def imports_group() -> CmemcGroup: # type: ignore[empty-body]
405
+ """List, create, delete and show graph imports.
406
+
407
+ Graphs are identified by an IRI. Statement imports are managed by
408
+ creating owl:imports statements such as "FROM_GRAPH owl:imports TO_GRAPH"
409
+ in the FROM_GRAPH. All statements in the TO_GRAPH are then available
410
+ in the FROM_GRAPH.
411
+
412
+ Note: The get a list of existing graphs,
413
+ execute the `graph list` command or use tab-completion.
414
+ """
415
+
416
+
417
+ imports_group.add_command(tree_command)
418
+ imports_group.add_command(list_command)
419
+ imports_group.add_command(create_command)
420
+ imports_group.add_command(delete_command)
@@ -202,9 +202,9 @@ def execute_command( # noqa: PLR0913
202
202
  Recipes are executed ordered by first_version.
203
203
 
204
204
  Here are some argument examples, in order to see how to use this command:
205
- `execute --all --test-only` will list all needed migrations (but not execute them),
206
- `execute --filter tag system` will apply all migrations which target system data,
207
- `execute bootstrap-data` will apply bootstrap-data migration if needed.
205
+ execute --all --test-only will list all needed migrations (but not execute them),
206
+ execute --filter tag system will apply all migrations which target system data,
207
+ execute bootstrap-data will apply bootstrap-data migration if needed.
208
208
  """
209
209
  app: ApplicationContext = ctx.obj
210
210
  if not all_ and not migration_id and not filter_:
@@ -256,13 +256,13 @@ def migration() -> CmemcGroup: # type: ignore[empty-body]
256
256
  with regard to the target data, it migrates.
257
257
 
258
258
  The following tags are important:
259
- 'system' recipes target data structures
259
+ `system` recipes target data structures
260
260
  which are needed to run the most basic functionality properly. These recipes
261
261
  can and should be applied after each version upgrade.
262
- 'user' recipes can change user and / or customizing data.
263
- 'acl' recipes migrate access condition data.
264
- 'shapes' recipes migrate shape data.
265
- 'config' recipes migrate configuration data.
262
+ `user` recipes can change user and / or customizing data.
263
+ `acl` recipes migrate access condition data.
264
+ `shapes` recipes migrate shape data.
265
+ `config` recipes migrate configuration data.
266
266
  """
267
267
 
268
268
 
@@ -1,4 +1,4 @@
1
- """DataIntegration project commands for the cmem command line interface."""
1
+ """Build (DataIntegration) project commands for the cmem command line interface."""
2
2
 
3
3
  import os
4
4
  import pathlib
@@ -1,4 +1,4 @@
1
- """DataIntegration python management commands."""
1
+ """Build (DataIntegration) python management commands."""
2
2
 
3
3
  import sys
4
4
  from dataclasses import asdict
@@ -297,9 +297,10 @@ def open_command(app: ApplicationContext, package: str) -> None:
297
297
  def reload_command(app: ApplicationContext) -> None:
298
298
  """Reload / Register all installed plugins.
299
299
 
300
- This command will register all installed plugins into the DataIntegration workspace.
300
+ This command will register all installed plugins into the Build
301
+ (DataIntegration) workspace.
301
302
  This command is useful, when you are installing packages
302
- into the DataIntegration Python environment without using the provided cmemc
303
+ into the Build Python environment without using the provided cmemc
303
304
  commands (e.g. by mounting a prepared filesystem in the docker container).
304
305
  """
305
306
  app.echo_info("Reloading python packages ... ", nl=False)
@@ -316,7 +317,7 @@ def reload_command(app: ApplicationContext) -> None:
316
317
  def python() -> CmemcGroup: # type: ignore[empty-body]
317
318
  """List, install, or uninstall python packages.
318
319
 
319
- Python packages are used to extend the DataIntegration workspace
320
+ Python packages are used to extend the Build (DataIntegration) workspace
320
321
  with python plugins. To get a list of installed packages, execute the
321
322
  list command.
322
323
 
@@ -548,7 +548,8 @@ def status_command(
548
548
  """Get status information of executed and running queries.
549
549
 
550
550
  With this command, you can access the latest executed SPARQL queries
551
- on the DataPlatform. These queries are identified by UUIDs and listed
551
+ on the Explore backend (DataPlatform).
552
+ These queries are identified by UUIDs and listed
552
553
  ordered by starting timestamp.
553
554
 
554
555
  You can filter queries based on status and runtime in order to investigate
@@ -1,4 +1,4 @@
1
- """DataIntegration dataset resource commands for cmemc."""
1
+ """Build dataset resource commands for cmemc."""
2
2
 
3
3
  import re
4
4
 
@@ -1,4 +1,4 @@
1
- """DataIntegration scheduler commands for the cmem command line interface."""
1
+ """Build scheduler commands for the cmem command line interface."""
2
2
 
3
3
  from typing import Any
4
4
 
@@ -1,4 +1,4 @@
1
- """DataPlatform store commands for the cmem command line interface."""
1
+ """Explore backend (DataPlatform) store commands for the cmem command line interface."""
2
2
 
3
3
  import os
4
4
  from dataclasses import dataclass
@@ -1,4 +1,4 @@
1
- """DataIntegration variable commands for cmemc."""
1
+ """Build (DataIntegration) variable commands for cmemc."""
2
2
 
3
3
  import re
4
4
 
@@ -51,8 +51,15 @@ FILE_EXTENSIONS_TO_PLUGIN_ID = {
51
51
  ".json": "json",
52
52
  ".xml": "xml",
53
53
  ".txt": "text",
54
+ ".md": "text",
54
55
  ".xlsx": "excel",
55
56
  ".zip": "multiCsv",
57
+ ".pdf": "binaryFile",
58
+ ".png": "binaryFile",
59
+ ".jpg": "binaryFile",
60
+ ".jpeg": "binaryFile",
61
+ ".gif": "binaryFile",
62
+ ".tiff": "binaryFile",
56
63
  }
57
64
 
58
65
  # Derive valid extensions from FILE_EXTENSIONS_TO_PLUGIN_ID keys
@@ -63,6 +70,7 @@ EXTRA_INPUT_MIME_TYPES = [
63
70
  "application/json",
64
71
  "application/xml",
65
72
  "text/csv",
73
+ "application/octet-stream",
66
74
  ]
67
75
 
68
76
  EXTRA_OUTPUT_MIME_TYPES = [
@@ -71,6 +79,7 @@ EXTRA_OUTPUT_MIME_TYPES = [
71
79
  "application/n-triples",
72
80
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
73
81
  "text/csv",
82
+ "application/octet-stream",
74
83
  ]
75
84
 
76
85
  STDOUT_UNSUPPORTED_MIME_TYPES = {