cmem-cmemc 25.3.0__py3-none-any.whl → 25.5.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/command_group.py +1 -0
- cmem_cmemc/commands/acl.py +80 -2
- cmem_cmemc/commands/admin.py +31 -4
- cmem_cmemc/commands/graph.py +12 -5
- cmem_cmemc/commands/graph_imports.py +3 -6
- cmem_cmemc/commands/graph_insights.py +314 -0
- cmem_cmemc/commands/query.py +54 -19
- cmem_cmemc/commands/store.py +6 -2
- cmem_cmemc/commands/validation.py +5 -3
- cmem_cmemc/commands/workflow.py +1 -0
- cmem_cmemc/completion.py +17 -18
- cmem_cmemc/context.py +2 -2
- cmem_cmemc/manual_helper/multi_page.py +2 -0
- cmem_cmemc/string_processor.py +8 -3
- {cmem_cmemc-25.3.0.dist-info → cmem_cmemc-25.5.0rc1.dist-info}/METADATA +6 -4
- {cmem_cmemc-25.3.0.dist-info → cmem_cmemc-25.5.0rc1.dist-info}/RECORD +19 -18
- {cmem_cmemc-25.3.0.dist-info → cmem_cmemc-25.5.0rc1.dist-info}/WHEEL +1 -1
- {cmem_cmemc-25.3.0.dist-info → cmem_cmemc-25.5.0rc1.dist-info}/entry_points.txt +0 -0
- {cmem_cmemc-25.3.0.dist-info → cmem_cmemc-25.5.0rc1.dist-info/licenses}/LICENSE +0 -0
cmem_cmemc/command_group.py
CHANGED
|
@@ -66,6 +66,7 @@ class CmemcGroup(HelpColorsGroup, DYMGroup):
|
|
|
66
66
|
"migrate": self.color_for_writing_commands,
|
|
67
67
|
"migrations": self.color_for_command_groups,
|
|
68
68
|
"imports": self.color_for_command_groups,
|
|
69
|
+
"insights": self.color_for_command_groups,
|
|
69
70
|
},
|
|
70
71
|
)
|
|
71
72
|
super().__init__(*args, **kwargs)
|
cmem_cmemc/commands/acl.py
CHANGED
|
@@ -35,6 +35,21 @@ HELP_TEXTS = {
|
|
|
35
35
|
"read_graph": "Grants read access to a graph.",
|
|
36
36
|
"write_graph": "Grants write access to a graph (includes read access).",
|
|
37
37
|
"action": "Grants usage permissions to an action / functionality.",
|
|
38
|
+
"read_graph_pattern": (
|
|
39
|
+
"Grants management of conditions granting read access on graphs matching the defined "
|
|
40
|
+
"pattern. A pattern consists of a constant string and a wildcard ('*') at the end of "
|
|
41
|
+
"the pattern or the wildcard alone."
|
|
42
|
+
),
|
|
43
|
+
"write_graph_pattern": (
|
|
44
|
+
"Grants management of conditions granting write access on graphs matching the defined "
|
|
45
|
+
"pattern. A pattern consists of a constant string and a wildcard ('*') at the end of "
|
|
46
|
+
"the pattern or the wildcard alone."
|
|
47
|
+
),
|
|
48
|
+
"action_pattern": (
|
|
49
|
+
"Grants management of conditions granting action allowance for actions matching the "
|
|
50
|
+
"defined pattern. A pattern consists of a constant string and a wildcard ('*') at the "
|
|
51
|
+
"end of the pattern or the wildcard alone."
|
|
52
|
+
),
|
|
38
53
|
"query": "Dynamic access condition query (file or the query catalog IRI).",
|
|
39
54
|
}
|
|
40
55
|
|
|
@@ -194,6 +209,27 @@ def inspect_command(app: ApplicationContext, access_condition_id: str, raw: bool
|
|
|
194
209
|
shell_complete=completion.acl_actions,
|
|
195
210
|
help=HELP_TEXTS["action"],
|
|
196
211
|
)
|
|
212
|
+
@click.option(
|
|
213
|
+
"--read-graph-pattern",
|
|
214
|
+
"read_graph_patterns",
|
|
215
|
+
type=click.STRING,
|
|
216
|
+
multiple=True,
|
|
217
|
+
help=HELP_TEXTS["read_graph_pattern"],
|
|
218
|
+
)
|
|
219
|
+
@click.option(
|
|
220
|
+
"--write-graph-pattern",
|
|
221
|
+
"write_graph_patterns",
|
|
222
|
+
type=click.STRING,
|
|
223
|
+
multiple=True,
|
|
224
|
+
help=HELP_TEXTS["write_graph_pattern"],
|
|
225
|
+
)
|
|
226
|
+
@click.option(
|
|
227
|
+
"--action-pattern",
|
|
228
|
+
"action_patterns",
|
|
229
|
+
type=click.STRING,
|
|
230
|
+
multiple=True,
|
|
231
|
+
help=HELP_TEXTS["action_pattern"],
|
|
232
|
+
)
|
|
197
233
|
@click.option(
|
|
198
234
|
"--query",
|
|
199
235
|
"query",
|
|
@@ -231,6 +267,9 @@ def create_command( # noqa: PLR0913
|
|
|
231
267
|
read_graphs: tuple[str],
|
|
232
268
|
write_graphs: tuple[str],
|
|
233
269
|
actions: tuple[str],
|
|
270
|
+
read_graph_patterns: tuple[str],
|
|
271
|
+
write_graph_patterns: tuple[str],
|
|
272
|
+
action_patterns: tuple[str],
|
|
234
273
|
query: str,
|
|
235
274
|
) -> None:
|
|
236
275
|
"""Create an access condition.
|
|
@@ -254,10 +293,19 @@ def create_command( # noqa: PLR0913
|
|
|
254
293
|
|
|
255
294
|
Example: cmemc admin acl create --group local-users --write-graph https://example.org/
|
|
256
295
|
"""
|
|
257
|
-
if
|
|
296
|
+
if (
|
|
297
|
+
not read_graphs
|
|
298
|
+
and not write_graphs
|
|
299
|
+
and not actions
|
|
300
|
+
and not read_graph_patterns
|
|
301
|
+
and not write_graph_patterns
|
|
302
|
+
and not action_patterns
|
|
303
|
+
and not query
|
|
304
|
+
):
|
|
258
305
|
raise click.UsageError(
|
|
259
306
|
"Missing access / usage grant. Use at least one of the following options: "
|
|
260
|
-
"--read-graph, --write-graph, --action
|
|
307
|
+
"--read-graph, --write-graph, --action, --read-graph-pattern, "
|
|
308
|
+
"--write-graph-pattern, --action-pattern or --query."
|
|
261
309
|
)
|
|
262
310
|
query_str = None
|
|
263
311
|
if query:
|
|
@@ -286,6 +334,9 @@ def create_command( # noqa: PLR0913
|
|
|
286
334
|
read_graphs=list(read_graphs),
|
|
287
335
|
write_graphs=list(write_graphs),
|
|
288
336
|
actions=[convert_qname_to_iri(qname=_, default_ns=NS_ACTION) for _ in actions],
|
|
337
|
+
read_graph_patterns=list(read_graph_patterns),
|
|
338
|
+
write_graph_patterns=list(write_graph_patterns),
|
|
339
|
+
action_patterns=list(action_patterns),
|
|
289
340
|
query=query_str,
|
|
290
341
|
)
|
|
291
342
|
app.echo_success("done")
|
|
@@ -351,6 +402,27 @@ def create_command( # noqa: PLR0913
|
|
|
351
402
|
shell_complete=completion.acl_actions,
|
|
352
403
|
help=HELP_TEXTS["action"],
|
|
353
404
|
)
|
|
405
|
+
@click.option(
|
|
406
|
+
"--read-graph-pattern",
|
|
407
|
+
"read_graph_patterns",
|
|
408
|
+
type=click.STRING,
|
|
409
|
+
multiple=True,
|
|
410
|
+
help=HELP_TEXTS["read_graph_pattern"],
|
|
411
|
+
)
|
|
412
|
+
@click.option(
|
|
413
|
+
"--write-graph-pattern",
|
|
414
|
+
"write_graph_patterns",
|
|
415
|
+
type=click.STRING,
|
|
416
|
+
multiple=True,
|
|
417
|
+
help=HELP_TEXTS["write_graph_pattern"],
|
|
418
|
+
)
|
|
419
|
+
@click.option(
|
|
420
|
+
"--action-pattern",
|
|
421
|
+
"action_patterns",
|
|
422
|
+
type=click.STRING,
|
|
423
|
+
multiple=True,
|
|
424
|
+
help=HELP_TEXTS["action_pattern"],
|
|
425
|
+
)
|
|
354
426
|
@click.option(
|
|
355
427
|
"--query",
|
|
356
428
|
"query",
|
|
@@ -370,6 +442,9 @@ def update_command( # noqa: PLR0913
|
|
|
370
442
|
read_graphs: tuple[str],
|
|
371
443
|
write_graphs: tuple[str],
|
|
372
444
|
actions: tuple[str],
|
|
445
|
+
read_graph_patterns: tuple[str],
|
|
446
|
+
write_graph_patterns: tuple[str],
|
|
447
|
+
action_patterns: tuple[str],
|
|
373
448
|
query: str,
|
|
374
449
|
) -> None:
|
|
375
450
|
"""Update an access condition.
|
|
@@ -396,6 +471,9 @@ def update_command( # noqa: PLR0913
|
|
|
396
471
|
read_graphs=read_graphs,
|
|
397
472
|
write_graphs=write_graphs,
|
|
398
473
|
actions=[convert_qname_to_iri(qname=_, default_ns=NS_ACTION) for _ in actions],
|
|
474
|
+
read_graph_patterns=read_graph_patterns,
|
|
475
|
+
write_graph_patterns=write_graph_patterns,
|
|
476
|
+
action_patterns=action_patterns,
|
|
399
477
|
query=query_str,
|
|
400
478
|
)
|
|
401
479
|
app.echo_success("done")
|
cmem_cmemc/commands/admin.py
CHANGED
|
@@ -4,6 +4,7 @@ from datetime import datetime, timezone
|
|
|
4
4
|
|
|
5
5
|
import click
|
|
6
6
|
import jwt
|
|
7
|
+
import timeago
|
|
7
8
|
from click import ClickException
|
|
8
9
|
from cmem.cmempy.api import get_access_token, get_token
|
|
9
10
|
from cmem.cmempy.config import get_cmem_base_uri
|
|
@@ -185,8 +186,14 @@ def status_command( # noqa: C901, PLR0912
|
|
|
185
186
|
help="Decode the access token and outputs the raw JSON. Note that the "
|
|
186
187
|
"access token is only decoded and esp. not validated.",
|
|
187
188
|
)
|
|
189
|
+
@click.option(
|
|
190
|
+
"--ttl",
|
|
191
|
+
is_flag=True,
|
|
192
|
+
help="Output information about the lifetime of the access token. "
|
|
193
|
+
"In combination with --raw, it outputs the TTL in seconds.",
|
|
194
|
+
)
|
|
188
195
|
@click.pass_obj
|
|
189
|
-
def token_command(app: ApplicationContext, raw: bool, decode: bool) -> None:
|
|
196
|
+
def token_command(app: ApplicationContext, raw: bool, decode: bool, ttl: bool) -> None:
|
|
190
197
|
"""Fetch and output an access token.
|
|
191
198
|
|
|
192
199
|
This command can be used to check for correct authentication as well as
|
|
@@ -202,9 +209,30 @@ def token_command(app: ApplicationContext, raw: bool, decode: bool) -> None:
|
|
|
202
209
|
# - get_access_token returns the token string which is maybe from conf
|
|
203
210
|
# - get_token fetches a new token incl. envelope from keycloak
|
|
204
211
|
|
|
212
|
+
token = get_access_token()
|
|
213
|
+
decoded_token = jwt.decode(token, options={"verify_signature": False})
|
|
214
|
+
if ttl:
|
|
215
|
+
app.echo_debug(token)
|
|
216
|
+
iat_ts = decoded_token["iat"]
|
|
217
|
+
exp_ts = decoded_token["exp"]
|
|
218
|
+
ttl_in_seconds = exp_ts - iat_ts
|
|
219
|
+
if raw:
|
|
220
|
+
app.echo_info_json(ttl_in_seconds)
|
|
221
|
+
return
|
|
222
|
+
exp_time = datetime.fromtimestamp(exp_ts, tz=timezone.utc)
|
|
223
|
+
now_time = datetime.now(tz=timezone.utc)
|
|
224
|
+
ttl_delta = timeago.format(exp_time, now_time)
|
|
225
|
+
if ttl_delta.startswith("in"):
|
|
226
|
+
app.echo_info(
|
|
227
|
+
f"The provided access token will expire {ttl_delta} "
|
|
228
|
+
f"(TTL is {ttl_in_seconds} seconds)."
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
app.echo_info(
|
|
232
|
+
f"The provided access token expired {ttl_delta} (TTL was {ttl_in_seconds} seconds)."
|
|
233
|
+
)
|
|
234
|
+
return
|
|
205
235
|
if decode:
|
|
206
|
-
token = get_access_token()
|
|
207
|
-
decoded_token = jwt.decode(token, options={"verify_signature": False})
|
|
208
236
|
if raw:
|
|
209
237
|
app.echo_info_json(decoded_token)
|
|
210
238
|
return
|
|
@@ -214,7 +242,6 @@ def token_command(app: ApplicationContext, raw: bool, decode: bool) -> None:
|
|
|
214
242
|
if raw:
|
|
215
243
|
app.echo_info_json(get_token())
|
|
216
244
|
return
|
|
217
|
-
token = get_access_token()
|
|
218
245
|
app.echo_info(token)
|
|
219
246
|
|
|
220
247
|
|
cmem_cmemc/commands/graph.py
CHANGED
|
@@ -27,6 +27,7 @@ from cmem_cmemc import completion
|
|
|
27
27
|
from cmem_cmemc.command import CmemcCommand
|
|
28
28
|
from cmem_cmemc.command_group import CmemcGroup
|
|
29
29
|
from cmem_cmemc.commands.graph_imports import graph_imports_list, imports_group
|
|
30
|
+
from cmem_cmemc.commands.graph_insights import insights_group
|
|
30
31
|
from cmem_cmemc.commands.validation import validation_group
|
|
31
32
|
from cmem_cmemc.constants import UNKNOWN_GRAPH_ERROR
|
|
32
33
|
from cmem_cmemc.context import ApplicationContext
|
|
@@ -567,10 +568,11 @@ def _get_graph_supported_formats() -> dict[str, str]:
|
|
|
567
568
|
|
|
568
569
|
def _get_buffer_and_content_type(
|
|
569
570
|
triple_file: str, app: ApplicationContext
|
|
570
|
-
) -> tuple[io.BytesIO, str]:
|
|
571
|
-
"""Get the io.BytesIO buffer and the content
|
|
571
|
+
) -> tuple[io.BytesIO, str, None | str]:
|
|
572
|
+
"""Get the io.BytesIO buffer, the content type and the content encoding of a triple_file"""
|
|
572
573
|
smart_file = SmartPath(triple_file)
|
|
573
574
|
content_type, encoding = mimetypes.guess_type(triple_file)
|
|
575
|
+
content_encoding = "gzip" if smart_file.name.endswith(".gz") else None
|
|
574
576
|
if content_type is None:
|
|
575
577
|
content_type = "text/turtle"
|
|
576
578
|
for supported_type, supported_suffix in _get_graph_supported_formats().items():
|
|
@@ -596,7 +598,7 @@ def _get_buffer_and_content_type(
|
|
|
596
598
|
with ClickSmartPath.open(triple_file, transport_params=transport_params) as file_obj:
|
|
597
599
|
buffer.write(file_obj.read())
|
|
598
600
|
buffer.seek(0)
|
|
599
|
-
return buffer, content_type
|
|
601
|
+
return buffer, content_type, content_encoding
|
|
600
602
|
|
|
601
603
|
|
|
602
604
|
def _create_graph_imports(ctx: Context, graphs: list[RdfGraphData]) -> None:
|
|
@@ -748,9 +750,13 @@ def import_command( # noqa: PLR0913
|
|
|
748
750
|
continue
|
|
749
751
|
# prevents re-replacing of graphs in a single run
|
|
750
752
|
_replace = False if graph_iri in processed_graphs else replace
|
|
751
|
-
_buffer, content_type = _get_buffer_and_content_type(triple_file, app)
|
|
753
|
+
_buffer, content_type, content_encoding = _get_buffer_and_content_type(triple_file, app)
|
|
752
754
|
response = graph_api.post_streamed(
|
|
753
|
-
graph_iri,
|
|
755
|
+
graph_iri,
|
|
756
|
+
_buffer,
|
|
757
|
+
replace=_replace,
|
|
758
|
+
content_type=content_type,
|
|
759
|
+
content_encoding=content_encoding,
|
|
754
760
|
)
|
|
755
761
|
request_headers = response.request.headers
|
|
756
762
|
request_headers.pop("Authorization")
|
|
@@ -886,3 +892,4 @@ graph.add_command(import_command)
|
|
|
886
892
|
graph.add_command(open_command)
|
|
887
893
|
graph.add_command(validation_group)
|
|
888
894
|
graph.add_command(imports_group)
|
|
895
|
+
graph.add_command(insights_group)
|
|
@@ -12,7 +12,6 @@ from treelib import Tree
|
|
|
12
12
|
from cmem_cmemc import completion
|
|
13
13
|
from cmem_cmemc.command import CmemcCommand
|
|
14
14
|
from cmem_cmemc.command_group import CmemcGroup
|
|
15
|
-
from cmem_cmemc.completion import escape_colon
|
|
16
15
|
from cmem_cmemc.constants import UNKNOWN_GRAPH_ERROR
|
|
17
16
|
from cmem_cmemc.context import ApplicationContext
|
|
18
17
|
from cmem_cmemc.object_list import DirectValuePropertyFilter, ObjectList
|
|
@@ -299,7 +298,7 @@ def _validate_graphs(from_graph: str | None, to_graph: str | None) -> None:
|
|
|
299
298
|
def _from_graph_uris(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]:
|
|
300
299
|
"""Provide auto completion items for delete command from-graph argument"""
|
|
301
300
|
imports = get_imports_list(ctx)
|
|
302
|
-
from_graphs = {
|
|
301
|
+
from_graphs = {_["from_graph"] for _ in imports}
|
|
303
302
|
return [
|
|
304
303
|
_
|
|
305
304
|
for _ in completion.graph_uris(ctx=ctx, param=param, incomplete=incomplete)
|
|
@@ -311,15 +310,13 @@ def _to_graph_uris(ctx: Context, param: Argument, incomplete: str) -> list[Compl
|
|
|
311
310
|
"""Provide auto completion items for create/delete command to-graph argument"""
|
|
312
311
|
from_graph = ctx.params["from_graph"]
|
|
313
312
|
imports = graph_imports_list.apply_filters(ctx=ctx, filter_=[("from-graph", from_graph)])
|
|
314
|
-
to_graphs = {
|
|
313
|
+
to_graphs = {_["to_graph"] for _ in imports}
|
|
315
314
|
command = ctx.command.name
|
|
316
315
|
return [
|
|
317
316
|
_
|
|
318
317
|
for _ in completion.graph_uris(ctx=ctx, param=param, incomplete=incomplete)
|
|
319
318
|
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
|
-
)
|
|
319
|
+
or (command == "create" and _.value not in to_graphs and _.value != from_graph)
|
|
323
320
|
]
|
|
324
321
|
|
|
325
322
|
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Graph Insights command group"""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from click import Argument, Context
|
|
7
|
+
from click.shell_completion import CompletionItem
|
|
8
|
+
from cmem.cmempy.api import get_json, request
|
|
9
|
+
from cmem.cmempy.config import get_dp_api_endpoint
|
|
10
|
+
from requests import HTTPError
|
|
11
|
+
|
|
12
|
+
from cmem_cmemc.command import CmemcCommand
|
|
13
|
+
from cmem_cmemc.command_group import CmemcGroup
|
|
14
|
+
from cmem_cmemc.completion import NOT_SORTED, finalize_completion, graph_uris
|
|
15
|
+
from cmem_cmemc.exceptions import CmemcError
|
|
16
|
+
from cmem_cmemc.object_list import (
|
|
17
|
+
DirectListPropertyFilter,
|
|
18
|
+
DirectValuePropertyFilter,
|
|
19
|
+
ObjectList,
|
|
20
|
+
transform_lower,
|
|
21
|
+
)
|
|
22
|
+
from cmem_cmemc.string_processor import GraphLink, TimeAgo
|
|
23
|
+
from cmem_cmemc.utils import get_graphs_as_dict, struct_to_table
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from cmem_cmemc.context import ApplicationContext
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
API_BASE = get_dp_api_endpoint() + "/api/ext/semspect"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_available() -> bool:
|
|
33
|
+
"""Check availability of graph insights endpoints
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
"isActive": true,
|
|
37
|
+
"isUserAllowed": true
|
|
38
|
+
}
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
data: dict[str, bool] = get_json(API_BASE)
|
|
42
|
+
except HTTPError:
|
|
43
|
+
return False
|
|
44
|
+
return bool(data["isActive"] is True and data["isUserAllowed"] is True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_availability(ctx: click.Context) -> None:
|
|
48
|
+
"""Check availability of graph insights endpoints or raise an exception"""
|
|
49
|
+
if is_available():
|
|
50
|
+
return
|
|
51
|
+
app: ApplicationContext = ctx.obj
|
|
52
|
+
raise CmemcError(app, "Graph Insights is not available.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_snapshots(ctx: click.Context) -> list[dict[str, str | bool | list[str]]]:
|
|
56
|
+
"""Get the snapshot list (all snapshots)"""
|
|
57
|
+
check_availability(ctx)
|
|
58
|
+
data: list[dict[str, str | bool | list[str]]] = get_json(
|
|
59
|
+
API_BASE + "/snapshot/status", params={"includeManagementOnly": True}
|
|
60
|
+
)
|
|
61
|
+
return data
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def complete_snapshot_ids(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]: # noqa: ARG001
|
|
65
|
+
"""Provide auto-completion for snapshot Ids"""
|
|
66
|
+
snapshots = get_snapshots(ctx)
|
|
67
|
+
snapshots = sorted(
|
|
68
|
+
snapshots, key=lambda snapshot: snapshot["updateInfoTimestamp"], reverse=True
|
|
69
|
+
)
|
|
70
|
+
options = [
|
|
71
|
+
(
|
|
72
|
+
snapshot["databaseId"],
|
|
73
|
+
f"{snapshot['mainGraphSynced']} ({snapshot['updateInfoTimestamp']})",
|
|
74
|
+
)
|
|
75
|
+
for snapshot in snapshots
|
|
76
|
+
]
|
|
77
|
+
return finalize_completion(candidates=options, incomplete=incomplete, sort_by=NOT_SORTED)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
snapshot_list = ObjectList(
|
|
81
|
+
name="insight snapshots",
|
|
82
|
+
get_objects=get_snapshots,
|
|
83
|
+
filters=[
|
|
84
|
+
DirectValuePropertyFilter(
|
|
85
|
+
name="id",
|
|
86
|
+
description="Snapshots with a specific id.",
|
|
87
|
+
property_key="databaseId",
|
|
88
|
+
transform=transform_lower,
|
|
89
|
+
),
|
|
90
|
+
DirectValuePropertyFilter(
|
|
91
|
+
name="main-graph",
|
|
92
|
+
description="Snapshots with a specific main graph.",
|
|
93
|
+
property_key="mainGraphSynced",
|
|
94
|
+
),
|
|
95
|
+
DirectValuePropertyFilter(
|
|
96
|
+
name="status",
|
|
97
|
+
description="Snapshots with a specific status.",
|
|
98
|
+
property_key="status",
|
|
99
|
+
),
|
|
100
|
+
DirectListPropertyFilter(
|
|
101
|
+
name="affected-graph",
|
|
102
|
+
description="Snapshots with a specific affected graph (main or sub-graphs).",
|
|
103
|
+
property_key="allGraphsSynced",
|
|
104
|
+
),
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@click.command(cls=CmemcCommand, name="list")
|
|
110
|
+
@click.option(
|
|
111
|
+
"--filter",
|
|
112
|
+
"filter_",
|
|
113
|
+
type=(str, str),
|
|
114
|
+
help=snapshot_list.get_filter_help_text(),
|
|
115
|
+
shell_complete=snapshot_list.complete_values,
|
|
116
|
+
multiple=True,
|
|
117
|
+
)
|
|
118
|
+
@click.option("--raw", is_flag=True, help="Outputs raw JSON response.")
|
|
119
|
+
@click.option(
|
|
120
|
+
"--id-only",
|
|
121
|
+
is_flag=True,
|
|
122
|
+
help="Return the snapshot IDs only. This is useful for piping the IDs into other commands.",
|
|
123
|
+
)
|
|
124
|
+
@click.pass_context
|
|
125
|
+
def list_command(ctx: Context, filter_: tuple[tuple[str, str]], id_only: bool, raw: bool) -> None:
|
|
126
|
+
"""List graph insight snapshots.
|
|
127
|
+
|
|
128
|
+
Graph Insights Snapshots are identified by an ID.
|
|
129
|
+
"""
|
|
130
|
+
check_availability(ctx)
|
|
131
|
+
app: ApplicationContext = ctx.obj
|
|
132
|
+
snapshots = snapshot_list.apply_filters(ctx=ctx, filter_=filter_)
|
|
133
|
+
|
|
134
|
+
if id_only:
|
|
135
|
+
for _ in snapshots:
|
|
136
|
+
click.echo(_["databaseId"])
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
if raw:
|
|
140
|
+
app.echo_info_json(snapshots)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
graphs = get_graphs_as_dict()
|
|
144
|
+
table = []
|
|
145
|
+
for _ in snapshots:
|
|
146
|
+
id_ = _["databaseId"]
|
|
147
|
+
main_graph = _["mainGraphSynced"]
|
|
148
|
+
updated = _["updateInfoTimestamp"]
|
|
149
|
+
status = _["status"]
|
|
150
|
+
if main_graph not in graphs:
|
|
151
|
+
main_graph = rf"\[missing: {main_graph}]"
|
|
152
|
+
table.append([id_, main_graph, updated, status])
|
|
153
|
+
|
|
154
|
+
app.echo_info_table(
|
|
155
|
+
table,
|
|
156
|
+
headers=["ID", "Main Graph", "Updated", "Status"],
|
|
157
|
+
sort_column=0,
|
|
158
|
+
cell_processing={1: GraphLink(), 2: TimeAgo()},
|
|
159
|
+
empty_table_message="No graph insight snapshots found.",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@click.command(cls=CmemcCommand, name="delete")
|
|
164
|
+
@click.argument("SNAPSHOT_ID", type=str, shell_complete=complete_snapshot_ids, required=False)
|
|
165
|
+
@click.option(
|
|
166
|
+
"--filter",
|
|
167
|
+
"filter_",
|
|
168
|
+
type=(str, str),
|
|
169
|
+
help=snapshot_list.get_filter_help_text(),
|
|
170
|
+
shell_complete=snapshot_list.complete_values,
|
|
171
|
+
multiple=True,
|
|
172
|
+
)
|
|
173
|
+
@click.option("-a", "--all", "all_", is_flag=True, help="Delete all snapshots.")
|
|
174
|
+
@click.pass_context
|
|
175
|
+
def delete_command(
|
|
176
|
+
ctx: Context, snapshot_id: str | None, filter_: tuple[tuple[str, str]], all_: bool
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Delete a graph insight snapshot.
|
|
179
|
+
|
|
180
|
+
Graph Insight Snapshots are identified by an ID.
|
|
181
|
+
To get a list of existing snapshots,
|
|
182
|
+
execute the `graph insights list` command or use tab-completion.
|
|
183
|
+
"""
|
|
184
|
+
check_availability(ctx)
|
|
185
|
+
app: ApplicationContext = ctx.obj
|
|
186
|
+
if snapshot_id is None and not filter_ and not all_:
|
|
187
|
+
raise click.UsageError("Either provide a snapshot ID or a filter, or use the --all flag.")
|
|
188
|
+
|
|
189
|
+
if all_:
|
|
190
|
+
app.echo_info("Deleting all snapshots ... ", nl=False)
|
|
191
|
+
request(method="DELETE", uri=f"{API_BASE}/snapshot")
|
|
192
|
+
app.echo_success("done")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
all_snapshots = get_snapshots(ctx)
|
|
196
|
+
all_snapshot_ids = [_["databaseId"] for _ in all_snapshots]
|
|
197
|
+
filter_to_apply = list(filter_) if filter_ else []
|
|
198
|
+
if snapshot_id:
|
|
199
|
+
filter_to_apply.append(("id", snapshot_id))
|
|
200
|
+
snapshots_to_delete = snapshot_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
|
|
201
|
+
if not snapshots_to_delete:
|
|
202
|
+
raise click.UsageError("No snapshots found to delete.")
|
|
203
|
+
|
|
204
|
+
for _ in snapshots_to_delete:
|
|
205
|
+
id_to_delete = _["databaseId"]
|
|
206
|
+
if id_to_delete not in all_snapshot_ids:
|
|
207
|
+
raise click.UsageError(f"Snapshot ID '{id_to_delete}' does not exist.")
|
|
208
|
+
for _ in snapshots_to_delete:
|
|
209
|
+
id_to_delete = _["databaseId"]
|
|
210
|
+
app.echo_info(f"Deleting snapshot {id_to_delete} ... ", nl=False)
|
|
211
|
+
request(method="DELETE", uri=f"{API_BASE}/snapshot/{id_to_delete}")
|
|
212
|
+
app.echo_success("done")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@click.command(cls=CmemcCommand, name="create")
|
|
216
|
+
@click.argument("iri", type=click.STRING, shell_complete=graph_uris)
|
|
217
|
+
@click.pass_context
|
|
218
|
+
def create_command(ctx: Context, iri: str) -> None:
|
|
219
|
+
"""Create or update a graph insight snapshot.
|
|
220
|
+
|
|
221
|
+
Create a graph insight snapshot for a given graph.
|
|
222
|
+
If the snapshot already exists, it is hot-swapped after re-creation.
|
|
223
|
+
The snapshot contains only the (imported) graphs the requesting user can read.
|
|
224
|
+
"""
|
|
225
|
+
check_availability(ctx)
|
|
226
|
+
app: ApplicationContext = ctx.obj
|
|
227
|
+
app.echo_info(f"Create / Update graph snapshot for graph {iri} ... ", nl=False)
|
|
228
|
+
request(method="POST", uri=f"{API_BASE}/snapshot", params={"contextGraph": iri})
|
|
229
|
+
app.echo_success("started")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@click.command(cls=CmemcCommand, name="update")
|
|
233
|
+
@click.argument("SNAPSHOT_ID", type=str, shell_complete=complete_snapshot_ids, required=False)
|
|
234
|
+
@click.option(
|
|
235
|
+
"--filter",
|
|
236
|
+
"filter_",
|
|
237
|
+
type=(str, str),
|
|
238
|
+
help=snapshot_list.get_filter_help_text(),
|
|
239
|
+
shell_complete=snapshot_list.complete_values,
|
|
240
|
+
multiple=True,
|
|
241
|
+
)
|
|
242
|
+
@click.option("-a", "--all", "all_", is_flag=True, help="Delete all snapshots.")
|
|
243
|
+
@click.pass_context
|
|
244
|
+
def update_command(
|
|
245
|
+
ctx: Context, snapshot_id: str | None, filter_: tuple[tuple[str, str]], all_: bool
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Update a graph insight snapshot.
|
|
248
|
+
|
|
249
|
+
Update a graph insight snapshot.
|
|
250
|
+
After the update, the snapshot is hot-swapped.
|
|
251
|
+
"""
|
|
252
|
+
check_availability(ctx)
|
|
253
|
+
app: ApplicationContext = ctx.obj
|
|
254
|
+
if snapshot_id is None and not filter_ and not all_:
|
|
255
|
+
raise click.UsageError("Either provide a snapshot ID or a filter, or use the --all flag.")
|
|
256
|
+
|
|
257
|
+
filter_to_apply = list(filter_) if filter_ else []
|
|
258
|
+
if snapshot_id:
|
|
259
|
+
filter_to_apply.append(("id", snapshot_id))
|
|
260
|
+
snapshots_to_update = snapshot_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
|
|
261
|
+
|
|
262
|
+
all_snapshots = get_snapshots(ctx)
|
|
263
|
+
all_snapshot_ids = [_["databaseId"] for _ in all_snapshots]
|
|
264
|
+
|
|
265
|
+
if all_:
|
|
266
|
+
snapshots_to_update = get_snapshots(ctx)
|
|
267
|
+
|
|
268
|
+
if not snapshots_to_update:
|
|
269
|
+
raise click.UsageError("No snapshots found to delete.")
|
|
270
|
+
|
|
271
|
+
for _ in snapshots_to_update:
|
|
272
|
+
id_to_update = _["databaseId"]
|
|
273
|
+
if id_to_update not in all_snapshot_ids:
|
|
274
|
+
raise click.UsageError(f"Snapshot ID '{id_to_update}' does not exist.")
|
|
275
|
+
for _ in snapshots_to_update:
|
|
276
|
+
id_to_update = _["databaseId"]
|
|
277
|
+
app.echo_info(f"Update snapshot {id_to_update} ... ", nl=False)
|
|
278
|
+
request(method="PUT", uri=f"{API_BASE}/snapshot/{snapshot_id}")
|
|
279
|
+
app.echo_success("started")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@click.command(cls=CmemcCommand, name="inspect")
|
|
283
|
+
@click.argument("SNAPSHOT_ID", type=str, shell_complete=complete_snapshot_ids)
|
|
284
|
+
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
285
|
+
@click.pass_context
|
|
286
|
+
def inspect_command(ctx: Context, snapshot_id: str, raw: bool) -> None:
|
|
287
|
+
"""Inspect the metadata of a graph insight snapshot."""
|
|
288
|
+
check_availability(ctx)
|
|
289
|
+
app: ApplicationContext = ctx.obj
|
|
290
|
+
snapshot: dict[str, str | bool | list[str]] = get_json(
|
|
291
|
+
f"{API_BASE}/snapshot/status/{snapshot_id}"
|
|
292
|
+
)
|
|
293
|
+
if raw:
|
|
294
|
+
app.echo_info_json(snapshot)
|
|
295
|
+
else:
|
|
296
|
+
table = struct_to_table(snapshot)
|
|
297
|
+
app.echo_info_table(table, headers=["Key", "Value"], sort_column=0)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@click.group(cls=CmemcGroup, name="insights")
|
|
301
|
+
def insights_group() -> CmemcGroup: # type: ignore[empty-body]
|
|
302
|
+
"""List, create, delete and inspect graph insight snapshots.
|
|
303
|
+
|
|
304
|
+
Graph Insight Snapshots are identified by an ID.
|
|
305
|
+
To get a list of existing snapshots,
|
|
306
|
+
execute the `graph insights list` command or use tab-completion.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
insights_group.add_command(list_command)
|
|
311
|
+
insights_group.add_command(delete_command)
|
|
312
|
+
insights_group.add_command(create_command)
|
|
313
|
+
insights_group.add_command(update_command)
|
|
314
|
+
insights_group.add_command(inspect_command)
|
cmem_cmemc/commands/query.py
CHANGED
|
@@ -10,8 +10,8 @@ from time import sleep, time
|
|
|
10
10
|
from uuid import uuid4
|
|
11
11
|
|
|
12
12
|
import click
|
|
13
|
-
from click import ClickException, UsageError
|
|
14
13
|
from click.shell_completion import CompletionItem
|
|
14
|
+
from cmem.cmempy.config import get_cmem_base_uri
|
|
15
15
|
from cmem.cmempy.queries import (
|
|
16
16
|
QueryCatalog,
|
|
17
17
|
SparqlQuery,
|
|
@@ -91,12 +91,12 @@ class ReplayStatistics:
|
|
|
91
91
|
iri = query_["iri"]
|
|
92
92
|
catalog_entry = self.catalog.get_query(iri)
|
|
93
93
|
if catalog_entry is None:
|
|
94
|
-
raise ClickException(f"measure_query - query {iri} is not in catalog.")
|
|
94
|
+
raise click.ClickException(f"measure_query - query {iri} is not in catalog.")
|
|
95
95
|
return catalog_entry
|
|
96
96
|
query_string = query_["queryString"]
|
|
97
97
|
return SparqlQuery(text=query_string)
|
|
98
98
|
except KeyError as error:
|
|
99
|
-
raise ClickException(
|
|
99
|
+
raise click.ClickException(
|
|
100
100
|
"measure_query - given input dict has no queryString key."
|
|
101
101
|
) from error
|
|
102
102
|
|
|
@@ -289,15 +289,15 @@ query_status_list = ObjectList(
|
|
|
289
289
|
compare=compare_regex,
|
|
290
290
|
fixed_completion=[
|
|
291
291
|
CompletionItem(
|
|
292
|
-
r"http
|
|
292
|
+
r"http://schema.org",
|
|
293
293
|
help="List only queries which somehow use the schema.org namespace.",
|
|
294
294
|
),
|
|
295
295
|
CompletionItem(
|
|
296
|
-
r"http
|
|
296
|
+
r"http://www.w3.org/2000/01/rdf-schema#",
|
|
297
297
|
help="List only queries which somehow use the RDF schema namespace.",
|
|
298
298
|
),
|
|
299
299
|
CompletionItem(
|
|
300
|
-
r"
|
|
300
|
+
r"?value",
|
|
301
301
|
help="List only queries which are using the ?value projection variable.",
|
|
302
302
|
),
|
|
303
303
|
CompletionItem(
|
|
@@ -330,6 +330,13 @@ def _output_query_status_details(app: ApplicationContext, status_dict: dict) ->
|
|
|
330
330
|
|
|
331
331
|
|
|
332
332
|
@click.command(cls=CmemcCommand, name="list")
|
|
333
|
+
@click.option(
|
|
334
|
+
"--catalog-graph",
|
|
335
|
+
default="https://ns.eccenca.com/data/queries/",
|
|
336
|
+
show_default=True,
|
|
337
|
+
shell_complete=completion.graph_uris,
|
|
338
|
+
help="The used query catalog graph.",
|
|
339
|
+
)
|
|
333
340
|
@click.option(
|
|
334
341
|
"--id-only",
|
|
335
342
|
is_flag=True,
|
|
@@ -337,13 +344,13 @@ def _output_query_status_details(app: ApplicationContext, status_dict: dict) ->
|
|
|
337
344
|
"This is useful for piping the ids into other cmemc commands.",
|
|
338
345
|
)
|
|
339
346
|
@click.pass_obj
|
|
340
|
-
def list_command(app: ApplicationContext, id_only: bool) -> None:
|
|
347
|
+
def list_command(app: ApplicationContext, catalog_graph: str, id_only: bool) -> None:
|
|
341
348
|
"""List available queries from the catalog.
|
|
342
349
|
|
|
343
350
|
Outputs a list of query URIs which can be used as reference for
|
|
344
351
|
the query execute command.
|
|
345
352
|
"""
|
|
346
|
-
queries = QueryCatalog().get_queries().items()
|
|
353
|
+
queries = QueryCatalog(graph=catalog_graph).get_queries().items()
|
|
347
354
|
if id_only:
|
|
348
355
|
# sort dict by short_url - https://docs.python.org/3/howto/sorting.html
|
|
349
356
|
for _, sparql_query in sorted(queries, key=lambda k: k[1].short_url.lower()):
|
|
@@ -359,7 +366,12 @@ def list_command(app: ApplicationContext, id_only: bool) -> None:
|
|
|
359
366
|
]
|
|
360
367
|
table.append(row)
|
|
361
368
|
app.echo_info_table(
|
|
362
|
-
table,
|
|
369
|
+
table,
|
|
370
|
+
headers=["Query URI", "Type", "Placeholder", "Label"],
|
|
371
|
+
sort_column=3,
|
|
372
|
+
empty_table_message="There are no query available in the "
|
|
373
|
+
f"selected catalog ({catalog_graph}).",
|
|
374
|
+
caption=f"Queries from {catalog_graph} ({get_cmem_base_uri()})",
|
|
363
375
|
)
|
|
364
376
|
|
|
365
377
|
|
|
@@ -367,6 +379,13 @@ def list_command(app: ApplicationContext, id_only: bool) -> None:
|
|
|
367
379
|
@click.argument(
|
|
368
380
|
"QUERIES", nargs=-1, required=True, shell_complete=completion.remote_queries_and_sparql_files
|
|
369
381
|
)
|
|
382
|
+
@click.option(
|
|
383
|
+
"--catalog-graph",
|
|
384
|
+
default="https://ns.eccenca.com/data/queries/",
|
|
385
|
+
show_default=True,
|
|
386
|
+
shell_complete=completion.graph_uris,
|
|
387
|
+
help="The used query catalog graph.",
|
|
388
|
+
)
|
|
370
389
|
@click.option(
|
|
371
390
|
"--accept",
|
|
372
391
|
default="default",
|
|
@@ -420,6 +439,7 @@ def list_command(app: ApplicationContext, id_only: bool) -> None:
|
|
|
420
439
|
def execute_command( # noqa: PLR0913
|
|
421
440
|
app: ApplicationContext,
|
|
422
441
|
queries: tuple[str, ...],
|
|
442
|
+
catalog_graph: str,
|
|
423
443
|
accept: str,
|
|
424
444
|
no_imports: bool,
|
|
425
445
|
base64: bool,
|
|
@@ -453,9 +473,14 @@ def execute_command( # noqa: PLR0913
|
|
|
453
473
|
app.echo_debug("Parameter: " + str(placeholder))
|
|
454
474
|
for file_or_uri in queries:
|
|
455
475
|
app.echo_debug(f"Start of execution: {file_or_uri} with " f"placeholder {placeholder}")
|
|
456
|
-
executed_query: SparqlQuery = QueryCatalog().get_query(
|
|
476
|
+
executed_query: SparqlQuery = QueryCatalog(graph=catalog_graph).get_query(
|
|
477
|
+
file_or_uri, placeholder=placeholder
|
|
478
|
+
)
|
|
457
479
|
if executed_query is None:
|
|
458
|
-
raise
|
|
480
|
+
raise click.UsageError(
|
|
481
|
+
f"{file_or_uri} is neither a (readable) file nor "
|
|
482
|
+
f"a query URI in the catalog graph {catalog_graph}"
|
|
483
|
+
)
|
|
459
484
|
app.echo_debug(
|
|
460
485
|
f"Execute ({executed_query.query_type}): "
|
|
461
486
|
f"{executed_query.label} < {executed_query.url}"
|
|
@@ -499,8 +524,15 @@ def execute_command( # noqa: PLR0913
|
|
|
499
524
|
@click.argument(
|
|
500
525
|
"QUERIES", nargs=-1, required=True, shell_complete=completion.remote_queries_and_sparql_files
|
|
501
526
|
)
|
|
527
|
+
@click.option(
|
|
528
|
+
"--catalog-graph",
|
|
529
|
+
default="https://ns.eccenca.com/data/queries/",
|
|
530
|
+
show_default=True,
|
|
531
|
+
shell_complete=completion.graph_uris,
|
|
532
|
+
help="The used query catalog graph.",
|
|
533
|
+
)
|
|
502
534
|
@click.pass_obj
|
|
503
|
-
def open_command(app: ApplicationContext, queries: tuple[str, ...]) -> None:
|
|
535
|
+
def open_command(app: ApplicationContext, queries: tuple[str, ...], catalog_graph: str) -> None:
|
|
504
536
|
"""Open queries in the editor of the query catalog in your browser.
|
|
505
537
|
|
|
506
538
|
With this command, you can open (remote) queries from the query catalog in
|
|
@@ -512,10 +544,13 @@ def open_command(app: ApplicationContext, queries: tuple[str, ...]) -> None:
|
|
|
512
544
|
opening multiple browser tabs.
|
|
513
545
|
"""
|
|
514
546
|
for file_or_uri in queries:
|
|
515
|
-
opened_query = QueryCatalog().get_query(file_or_uri)
|
|
547
|
+
opened_query = QueryCatalog(graph=catalog_graph).get_query(file_or_uri)
|
|
516
548
|
if opened_query is None:
|
|
517
|
-
raise
|
|
518
|
-
|
|
549
|
+
raise click.UsageError(
|
|
550
|
+
f"{file_or_uri} is neither a (readable) file nor "
|
|
551
|
+
f"a query URI in the catalog graph {catalog_graph}"
|
|
552
|
+
)
|
|
553
|
+
open_query_uri = opened_query.get_editor_url(graph=catalog_graph)
|
|
519
554
|
app.echo_debug(f"Open {file_or_uri}: {open_query_uri}")
|
|
520
555
|
click.launch(open_query_uri)
|
|
521
556
|
|
|
@@ -571,7 +606,7 @@ def status_command(
|
|
|
571
606
|
queries = query_status_list.apply_filters(ctx=ctx, filter_=filter_)
|
|
572
607
|
|
|
573
608
|
if query_id and len(queries) == 0:
|
|
574
|
-
raise UsageError(f"Query with ID '{query_id}' does not exist (anymore).")
|
|
609
|
+
raise click.UsageError(f"Query with ID '{query_id}' does not exist (anymore).")
|
|
575
610
|
|
|
576
611
|
if raw:
|
|
577
612
|
app.echo_info_json(queries)
|
|
@@ -666,14 +701,14 @@ def replay_command( # noqa: PLR0913
|
|
|
666
701
|
other data.
|
|
667
702
|
"""
|
|
668
703
|
if loops <= 0:
|
|
669
|
-
raise UsageError("Please set a positive loops integer value (>=1).")
|
|
704
|
+
raise click.UsageError("Please set a positive loops integer value (>=1).")
|
|
670
705
|
try:
|
|
671
706
|
with Path(replay_file).open(encoding="utf8") as _:
|
|
672
707
|
input_queries = load(_)
|
|
673
708
|
except JSONDecodeError as error:
|
|
674
|
-
raise ClickException(f"File {replay_file} is not a valid JSON document.") from error
|
|
709
|
+
raise click.ClickException(f"File {replay_file} is not a valid JSON document.") from error
|
|
675
710
|
if len(input_queries) == 0:
|
|
676
|
-
raise ClickException(f"File {replay_file} contains no queries.")
|
|
711
|
+
raise click.ClickException(f"File {replay_file} contains no queries.")
|
|
677
712
|
app.echo_debug(f"File {replay_file} contains {len(input_queries)} queries.")
|
|
678
713
|
|
|
679
714
|
statistic = ReplayStatistics(app=app, label=run_label)
|
cmem_cmemc/commands/store.py
CHANGED
|
@@ -10,6 +10,7 @@ from cmem.cmempy.dp.admin import create_showcase_data, delete_bootstrap_data, im
|
|
|
10
10
|
from cmem.cmempy.dp.admin.backup import get_zip, post_zip
|
|
11
11
|
from cmem.cmempy.dp.workspace import migrate_workspaces
|
|
12
12
|
from cmem.cmempy.health import get_dp_info
|
|
13
|
+
from cmem.cmempy.workspace import reload_workspace
|
|
13
14
|
from jinja2 import Template
|
|
14
15
|
|
|
15
16
|
from cmem_cmemc.command import CmemcCommand
|
|
@@ -53,7 +54,7 @@ def bootstrap_command(app: ApplicationContext, import_: bool, remove: bool) -> N
|
|
|
53
54
|
|
|
54
55
|
Note: The import part of this command is equivalent to the 'bootstrap-data' migration recipe
|
|
55
56
|
"""
|
|
56
|
-
if import_ and remove or not import_ and not remove:
|
|
57
|
+
if (import_ and remove) or (not import_ and not remove):
|
|
57
58
|
raise UsageError("Either use the --import or the --remove option.")
|
|
58
59
|
if import_:
|
|
59
60
|
app.echo_info("Update or import bootstrap data ... ", nl=False)
|
|
@@ -99,12 +100,15 @@ def showcase_command(app: ApplicationContext, scale: int, create: bool, delete:
|
|
|
99
100
|
raise UsageError("Either use the --create or the --delete flag.")
|
|
100
101
|
if delete:
|
|
101
102
|
raise NotImplementedError(
|
|
102
|
-
"This feature is not implemented yet.
|
|
103
|
+
"This feature is not implemented yet. Please delete the graphs manually."
|
|
103
104
|
)
|
|
104
105
|
if create:
|
|
105
106
|
app.echo_info(f"Create showcase data with scale factor {scale} ... ", nl=False)
|
|
106
107
|
create_showcase_data(scale_factor=scale)
|
|
107
108
|
app.echo_success("done")
|
|
109
|
+
app.echo_info("Reload workspace ... ", nl=False)
|
|
110
|
+
reload_workspace()
|
|
111
|
+
app.echo_success("done")
|
|
108
112
|
|
|
109
113
|
|
|
110
114
|
@click.command(cls=CmemcCommand, name="export")
|
|
@@ -766,11 +766,13 @@ def export_command(
|
|
|
766
766
|
f" to {output_file} ... ",
|
|
767
767
|
nl=False,
|
|
768
768
|
)
|
|
769
|
-
|
|
769
|
+
output_path = Path(output_file)
|
|
770
|
+
|
|
771
|
+
with output_path.open("w", encoding="utf-8") as file:
|
|
770
772
|
if format_ == "XML":
|
|
771
|
-
file.
|
|
773
|
+
file.write(_reports_to_junit(reports))
|
|
772
774
|
if format_ == "JSON":
|
|
773
|
-
|
|
775
|
+
json.dump(reports, file, indent=2)
|
|
774
776
|
app.echo_success("done")
|
|
775
777
|
if exit_1 == "error" and overall_violations > 0:
|
|
776
778
|
app.echo_error(
|
cmem_cmemc/commands/workflow.py
CHANGED
cmem_cmemc/completion.py
CHANGED
|
@@ -53,11 +53,6 @@ SORT_BY_KEY = 0
|
|
|
53
53
|
SORT_BY_DESC = 1
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def escape_colon(value: str) -> str:
|
|
57
|
-
"""Escape colons in the input string by prefixing them with a backslash."""
|
|
58
|
-
return value.replace(":", r"\:")
|
|
59
|
-
|
|
60
|
-
|
|
61
56
|
def finalize_completion(
|
|
62
57
|
candidates: list,
|
|
63
58
|
incomplete: str = "",
|
|
@@ -130,10 +125,7 @@ def finalize_completion(
|
|
|
130
125
|
key=lambda x: (str(x[sort_by]).casefold(), str(x[sort_by])),
|
|
131
126
|
reverse=reverse,
|
|
132
127
|
)
|
|
133
|
-
return [
|
|
134
|
-
CompletionItem(value=escape_colon(element[0]), help=element[1])
|
|
135
|
-
for element in sorted_list
|
|
136
|
-
]
|
|
128
|
+
return [CompletionItem(value=element[0], help=element[1]) for element in sorted_list]
|
|
137
129
|
|
|
138
130
|
raise ClickException(
|
|
139
131
|
"Candidates should be a list of strings or a list of tuples." f" Got {candidates}"
|
|
@@ -199,7 +191,10 @@ def acl_actions(ctx: Context, param: Argument, incomplete: str) -> list[Completi
|
|
|
199
191
|
if check_option_in_params(iri, ctx.params.get(str(param.name))):
|
|
200
192
|
continue
|
|
201
193
|
options.append((qname, name))
|
|
202
|
-
|
|
194
|
+
if not check_option_in_params("urn:elds-backend-all-actions", ctx.params.get(str(param.name))):
|
|
195
|
+
options.append(
|
|
196
|
+
("urn:elds-backend-all-actions", "All Actions (until 24.2.x, now deprecated)")
|
|
197
|
+
)
|
|
203
198
|
return finalize_completion(candidates=options, incomplete=incomplete, sort_by=SORT_BY_DESC)
|
|
204
199
|
|
|
205
200
|
|
|
@@ -359,10 +354,10 @@ def dataset_list_filter(ctx: Context, param: Argument, incomplete: str) -> list[
|
|
|
359
354
|
("type", "Filter by dataset type."),
|
|
360
355
|
]
|
|
361
356
|
filter_regex = [
|
|
362
|
-
(
|
|
357
|
+
("^Final:", "Example: Dataset label starts with 'Final:'."),
|
|
363
358
|
(
|
|
364
359
|
r"[12][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]",
|
|
365
|
-
"Example: Dataset label contains a
|
|
360
|
+
"Example: Dataset label contains a date-like string.",
|
|
366
361
|
),
|
|
367
362
|
]
|
|
368
363
|
options = []
|
|
@@ -526,6 +521,7 @@ def get_dataset_file_mapping() -> dict[str, dict[str, str]]:
|
|
|
526
521
|
return {
|
|
527
522
|
".csv": {"description": "CSV Dataset resource", "type": "csv"},
|
|
528
523
|
".csv.zip": {"description": "CSV Dataset resource (zipped)", "type": "csv"},
|
|
524
|
+
".xls": {"description": "Excel Dataset resource", "type": "excel"},
|
|
529
525
|
".xlsx": {"description": "Excel Dataset resource", "type": "excel"},
|
|
530
526
|
".xml": {"description": "XML Dataset resource", "type": "xml"},
|
|
531
527
|
".xml.zip": {"description": "XML Dataset resource (zipped)", "type": "xml"},
|
|
@@ -643,10 +639,11 @@ def placeholder(ctx: Context, param: Argument, incomplete: str) -> list[Completi
|
|
|
643
639
|
args = get_completion_args(incomplete)
|
|
644
640
|
# setup configuration
|
|
645
641
|
ApplicationContext.set_connection_from_params(ctx.find_root().params)
|
|
642
|
+
catalog_graph = ctx.params.get("catalog_graph")
|
|
646
643
|
# extract placeholder from given queries in the command line
|
|
647
644
|
options = []
|
|
648
645
|
placeholders: dict[str, QueryPlaceholder] = {}
|
|
649
|
-
catalog = QueryCatalog()
|
|
646
|
+
catalog = QueryCatalog(graph=catalog_graph) if catalog_graph else QueryCatalog()
|
|
650
647
|
for _, arg in enumerate(args):
|
|
651
648
|
query = catalog.get_query(arg)
|
|
652
649
|
if query is not None:
|
|
@@ -654,7 +651,7 @@ def placeholder(ctx: Context, param: Argument, incomplete: str) -> list[Completi
|
|
|
654
651
|
placeholders = placeholders | get_placeholders_for_query(iri=query.url)
|
|
655
652
|
# collect all placeholder keys
|
|
656
653
|
options.extend(list(query.get_placeholder_keys()))
|
|
657
|
-
# look if cursor is in value position of the -p option and
|
|
654
|
+
# look if the cursor is in value position of the -p option and
|
|
658
655
|
# use placeholder value completion, in case it is
|
|
659
656
|
if args[len(args) - 2] in ("-p", "--parameter"):
|
|
660
657
|
key = args[len(args) - 1]
|
|
@@ -676,8 +673,10 @@ def placeholder(ctx: Context, param: Argument, incomplete: str) -> list[Completi
|
|
|
676
673
|
def remote_queries(ctx: Context, param: Argument, incomplete: str) -> list[CompletionItem]:
|
|
677
674
|
"""Prepare a list of query URIs."""
|
|
678
675
|
ApplicationContext.set_connection_from_params(ctx.find_root().params)
|
|
676
|
+
catalog_graph = ctx.params.get("catalog_graph")
|
|
677
|
+
catalog = QueryCatalog(graph=catalog_graph) if catalog_graph else QueryCatalog()
|
|
679
678
|
options = []
|
|
680
|
-
for query in
|
|
679
|
+
for query in catalog.get_queries().values():
|
|
681
680
|
url = query.short_url
|
|
682
681
|
label = query.label
|
|
683
682
|
options.append((url, label))
|
|
@@ -798,11 +797,11 @@ def graph_uris_with_all_graph_uri(
|
|
|
798
797
|
options = graph_uris(ctx, param, incomplete, writeable=True, readonly=True)
|
|
799
798
|
options.append(
|
|
800
799
|
CompletionItem(
|
|
801
|
-
value=r"urn
|
|
800
|
+
value=r"urn:elds-backend-all-graphs", help="All Graphs (until 24.2.x, now deprecated)"
|
|
802
801
|
)
|
|
803
802
|
)
|
|
804
803
|
options.append(
|
|
805
|
-
CompletionItem(value=r"https
|
|
804
|
+
CompletionItem(value=r"https://vocab.eccenca.com/auth/AllGraphs", help="All Graphs")
|
|
806
805
|
)
|
|
807
806
|
return options
|
|
808
807
|
|
|
@@ -958,7 +957,7 @@ def workflow_list_filter(ctx: Context, param: Argument, incomplete: str) -> list
|
|
|
958
957
|
("input-output", "List only workflows with a variable input and output dataset."),
|
|
959
958
|
]
|
|
960
959
|
filter_regex = [
|
|
961
|
-
(r"^Final
|
|
960
|
+
(r"^Final:", "Example: Workflow label starts with 'Final:'."),
|
|
962
961
|
(
|
|
963
962
|
r"[12][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]",
|
|
964
963
|
"Example: Workflow label contains a data-like string.",
|
cmem_cmemc/context.py
CHANGED
|
@@ -29,9 +29,9 @@ from cmem_cmemc.exceptions import InvalidConfigurationError
|
|
|
29
29
|
from cmem_cmemc.string_processor import StringProcessor, process_row
|
|
30
30
|
from cmem_cmemc.utils import is_enabled, str_to_bool
|
|
31
31
|
|
|
32
|
-
DI_TARGET_VERSION = "v25.
|
|
32
|
+
DI_TARGET_VERSION = "v25.2.0"
|
|
33
33
|
|
|
34
|
-
EXPLORE_TARGET_VERSION = "v25.
|
|
34
|
+
EXPLORE_TARGET_VERSION = "v25.2.0"
|
|
35
35
|
|
|
36
36
|
KNOWN_CONFIG_KEYS = {
|
|
37
37
|
"CMEM_BASE_URI": cmempy_config.get_cmem_base_uri,
|
|
@@ -31,6 +31,7 @@ def get_icon_for_command_group(full_name: str) -> str:
|
|
|
31
31
|
"project variable": "material/variable-box",
|
|
32
32
|
"query": "eccenca/application-queries",
|
|
33
33
|
"graph": "eccenca/artefact-dataset-eccencadataplatform",
|
|
34
|
+
"graph imports": "material/family-tree",
|
|
34
35
|
"graph validation": "octicons/verified-16",
|
|
35
36
|
"vocabulary": "eccenca/application-vocabularies",
|
|
36
37
|
"vocabulary cache": "eccenca/application-vocabularies",
|
|
@@ -58,6 +59,7 @@ def get_tags_for_command_group(full_name: str) -> str:
|
|
|
58
59
|
"project variable": ["Variables", "cmemc"],
|
|
59
60
|
"query": ["SPARQL", "cmemc"],
|
|
60
61
|
"graph": ["KnowledgeGraph", "cmemc"],
|
|
62
|
+
"graph imports": ["KnowledgeGraph", "cmemc"],
|
|
61
63
|
"graph validation": ["KnowledgeGraph", "Validation", "cmemc"],
|
|
62
64
|
"vocabulary": ["Vocabulary", "cmemc"],
|
|
63
65
|
"vocabulary cache": ["Vocabulary", "cmemc"],
|
cmem_cmemc/string_processor.py
CHANGED
|
@@ -20,18 +20,23 @@ class StringProcessor(ABC):
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class TimeAgo(StringProcessor):
|
|
23
|
-
"""Create a string similar to 'x minutes ago' from a timestamp"""
|
|
23
|
+
"""Create a string similar to 'x minutes ago' from a timestamp or iso-formated string."""
|
|
24
24
|
|
|
25
25
|
def process(self, text: str) -> str:
|
|
26
26
|
"""Process a single string content and output the processed string."""
|
|
27
27
|
if text is None:
|
|
28
28
|
return ""
|
|
29
|
+
try:
|
|
30
|
+
stamp = datetime.fromisoformat(str(text))
|
|
31
|
+
return str(timeago.format(stamp, datetime.now(tz=timezone.utc)))
|
|
32
|
+
except (ValueError, TypeError):
|
|
33
|
+
pass
|
|
29
34
|
try:
|
|
30
35
|
text_as_int = int(text)
|
|
36
|
+
stamp = datetime.fromtimestamp(text_as_int / 1000, tz=timezone.utc)
|
|
37
|
+
return str(timeago.format(stamp, datetime.now(tz=timezone.utc)))
|
|
31
38
|
except ValueError:
|
|
32
39
|
return text
|
|
33
|
-
stamp = datetime.fromtimestamp(text_as_int / 1000, tz=timezone.utc)
|
|
34
|
-
return str(timeago.format(stamp, datetime.now(tz=timezone.utc)))
|
|
35
40
|
|
|
36
41
|
|
|
37
42
|
class GraphLink(StringProcessor):
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: cmem-cmemc
|
|
3
|
-
Version: 25.
|
|
3
|
+
Version: 25.5.0rc1
|
|
4
4
|
Summary: Command line client for eccenca Corporate Memory
|
|
5
5
|
License: Apache-2.0
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: eccenca
|
|
7
8
|
Author-email: cmempy-developer@eccenca.com
|
|
8
9
|
Requires-Python: >=3.10,<4.0
|
|
@@ -21,16 +22,17 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
21
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
26
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
25
27
|
Classifier: Topic :: Database
|
|
26
28
|
Classifier: Topic :: Software Development :: Testing
|
|
27
29
|
Classifier: Topic :: Utilities
|
|
28
30
|
Requires-Dist: beautifulsoup4 (>=4.13.3,<5.0.0)
|
|
29
31
|
Requires-Dist: certifi (>=2024.2.2)
|
|
30
|
-
Requires-Dist: click (>=8.
|
|
32
|
+
Requires-Dist: click (>=8.3.0,<9.0.0)
|
|
31
33
|
Requires-Dist: click-didyoumean (>=0.3.1,<0.4.0)
|
|
32
34
|
Requires-Dist: click-help-colors (>=0.9.4,<0.10.0)
|
|
33
|
-
Requires-Dist: cmem-cmempy (==25.
|
|
35
|
+
Requires-Dist: cmem-cmempy (==25.4.0)
|
|
34
36
|
Requires-Dist: configparser (>=7.2.0,<8.0.0)
|
|
35
37
|
Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
|
|
36
38
|
Requires-Dist: junit-xml (>=1.9,<2.0)
|
|
@@ -2,38 +2,39 @@ cmem_cmemc/__init__.py,sha256=-RPEVweA-fcmEAynszDDMKwArJgxZpGW61UBiV7O4Og,24
|
|
|
2
2
|
cmem_cmemc/_cmemc.zsh,sha256=fmkrBHIQxus8cp2AgO1tzZ5mNZdGL_83cYz3a9uAdsg,1326
|
|
3
3
|
cmem_cmemc/cli.py,sha256=vDdSHFmXUstC3T7OlbPSd0hXxyigJE4VVgRMcsNz5cc,4538
|
|
4
4
|
cmem_cmemc/command.py,sha256=nBtrwPKFJLRpD3IPk5hKyn2LOMl-1ae7SV9iRhgky8k,1958
|
|
5
|
-
cmem_cmemc/command_group.py,sha256=
|
|
5
|
+
cmem_cmemc/command_group.py,sha256=0fG4A68Ic8NPCXj5mC-w42j6ryMPGxBBCHtLJzQHrn0,3488
|
|
6
6
|
cmem_cmemc/commands/__init__.py,sha256=NaGM5jOzf0S_-4UIAwlVDOf2AZ3mliGPoRLXQJfTyZs,22
|
|
7
|
-
cmem_cmemc/commands/acl.py,sha256=
|
|
8
|
-
cmem_cmemc/commands/admin.py,sha256=
|
|
7
|
+
cmem_cmemc/commands/acl.py,sha256=LlRoPT-c7AcqZBJ19XGVZZJPkCbM2A0RtlLzjMuDqIQ,18484
|
|
8
|
+
cmem_cmemc/commands/admin.py,sha256=F-393oXTVYV7HxK5NxuhONlBIEg7wffxE7DAKDuasG4,10192
|
|
9
9
|
cmem_cmemc/commands/client.py,sha256=nBs7MoF2wF45AteTCeIQrXcOwKmHHCd8_lG_SM2mQSA,5127
|
|
10
10
|
cmem_cmemc/commands/config.py,sha256=VHiVkW6NFuz-tpKXRPl7dO1gIXQLOuEhlGVxb422qwA,5803
|
|
11
11
|
cmem_cmemc/commands/dataset.py,sha256=zylVsfBD92LzHgQ96edNKaQNsf0zKoPro0VQdX--Mu8,30488
|
|
12
|
-
cmem_cmemc/commands/graph.py,sha256=
|
|
13
|
-
cmem_cmemc/commands/graph_imports.py,sha256=
|
|
12
|
+
cmem_cmemc/commands/graph.py,sha256=uAI7dEDRjwLSGl6QsJ0PsAycJf2-EZNNn_lIFliDE9Q,32662
|
|
13
|
+
cmem_cmemc/commands/graph_imports.py,sha256=CYgTUSj8giHoWzz0Qjtup0H1V5GKZEI3xXdA_9w_btI,14046
|
|
14
|
+
cmem_cmemc/commands/graph_insights.py,sha256=KDBhIVDxhmcAjI81pSEi2IG0km1SML_PeE7WTBY4moI,10810
|
|
14
15
|
cmem_cmemc/commands/manual.py,sha256=-sZWeFL92Kj8gL3VYsbpKh2ZaVTyM3LgKaUcpNn9u3A,2179
|
|
15
16
|
cmem_cmemc/commands/metrics.py,sha256=pIBRTq90f7MEI99HLdFLN3D1xQ2Z2u6VKUeTIz0X7DY,12205
|
|
16
17
|
cmem_cmemc/commands/migration.py,sha256=y9v4Be7WELGjDGDBZrfLBeqU_G_JH1fnP5UVG9qjB3g,9638
|
|
17
18
|
cmem_cmemc/commands/project.py,sha256=zHqs7XBsRjzTO6EGxaN_TgZ_rsqyIPF59aObuMhsfmA,20593
|
|
18
19
|
cmem_cmemc/commands/python.py,sha256=7ExdKr7nTED5ZZGjeBk5UngNI3F_KNcavwUgwaxApV0,11965
|
|
19
|
-
cmem_cmemc/commands/query.py,sha256=
|
|
20
|
+
cmem_cmemc/commands/query.py,sha256=Dra4BHshfec1nvVJbDvLT9VTjfesWJGSQCbyNmnnMWI,28788
|
|
20
21
|
cmem_cmemc/commands/resource.py,sha256=74cn_yqMv3a6xOQAPpNCuluTWEH-_2PGENJnl7y8qz4,7778
|
|
21
22
|
cmem_cmemc/commands/scheduler.py,sha256=zYeO1-Hlxh9D-I9JIweQ-SEA0la0wv0EicY_UY7rNCg,8751
|
|
22
|
-
cmem_cmemc/commands/store.py,sha256=
|
|
23
|
+
cmem_cmemc/commands/store.py,sha256=W_6LXq98If50-X-XYZUQsYodVwUjOSm3_jyMp62pFuA,10599
|
|
23
24
|
cmem_cmemc/commands/user.py,sha256=ANZpeOBA46xiqOcNPrueComsCV0gEBbav-vOL9VgyX4,12535
|
|
24
|
-
cmem_cmemc/commands/validation.py,sha256=
|
|
25
|
+
cmem_cmemc/commands/validation.py,sha256=ebolVeKpTTQ-tNjsmGWnfIlvv77lDwiWZRtd-qLbrHM,29509
|
|
25
26
|
cmem_cmemc/commands/variable.py,sha256=aLRH_rFe0h7JBpKIqzcevbk26vczgUGokIDY8g6LPxA,11576
|
|
26
27
|
cmem_cmemc/commands/vocabulary.py,sha256=fdXsG7gspA6HeOasXis1ky9UIZG-qRYP-NiFcvzCTKM,17840
|
|
27
|
-
cmem_cmemc/commands/workflow.py,sha256=
|
|
28
|
+
cmem_cmemc/commands/workflow.py,sha256=BINC-P5RsDvKTkHUbKZpzkfV5M12Cl7EPD4RLmygDOQ,25798
|
|
28
29
|
cmem_cmemc/commands/workspace.py,sha256=IcZgBsvtulLRFofS70qpln6oKQIZunrVLfSAUeiFhCA,4579
|
|
29
|
-
cmem_cmemc/completion.py,sha256=
|
|
30
|
+
cmem_cmemc/completion.py,sha256=k5sqbZtjcOBcrmCoottbav9WM-dzwkL4Qyu4QUaAuRo,45306
|
|
30
31
|
cmem_cmemc/config_parser.py,sha256=NduwOT-BB_uAk3pz1Y-ex18RQJW-jjHzkQKCEUUK6Hc,1276
|
|
31
32
|
cmem_cmemc/constants.py,sha256=pzZYbSaTDUiWmE-VOAHB20oivHew5_FP9UTejySsVK4,550
|
|
32
|
-
cmem_cmemc/context.py,sha256=
|
|
33
|
+
cmem_cmemc/context.py,sha256=giSw-5BYQtf_Tw0x33JlpzoZY5wP4IMaaoSJMvYzMSc,22247
|
|
33
34
|
cmem_cmemc/exceptions.py,sha256=0lsGOfXhciNGJloJGERMbbPuBbs5IwIIJ_5YnY9qQ-8,546
|
|
34
35
|
cmem_cmemc/manual_helper/__init__.py,sha256=G3Lqw2aPxo8x63Tg7L0aa5VD9BMaRzZDmhrog7IuEPg,43
|
|
35
36
|
cmem_cmemc/manual_helper/graph.py,sha256=HU04NYWeJ6LmW4UC7qHr1v1qsm2Md61pJ-pgWUHFmHY,3647
|
|
36
|
-
cmem_cmemc/manual_helper/multi_page.py,sha256=
|
|
37
|
+
cmem_cmemc/manual_helper/multi_page.py,sha256=_nw5yFVo7K1e9n5qETJcbkq12ru_Fq9TH0u8EzyyRz4,12338
|
|
37
38
|
cmem_cmemc/manual_helper/single_page.py,sha256=0mMn_IJwFCe-WPKAmxGEStb8IINLpQRxAx_F1pIxg1E,1526
|
|
38
39
|
cmem_cmemc/migrations/__init__.py,sha256=i6Ri7qN58ou_MwOzm2KibPkXOD7u-1ELky-nUE5LjAA,24
|
|
39
40
|
cmem_cmemc/migrations/abc.py,sha256=UGJzrvMzUFdp2-sosp49ObRI-SrUSzLJqLEhvB4QTzg,3564
|
|
@@ -50,11 +51,11 @@ cmem_cmemc/placeholder.py,sha256=PzZWpVBa7WPP5_5f-HlSoaZaOnto-1Dr9XuCTSpPnNM,238
|
|
|
50
51
|
cmem_cmemc/smart_path/__init__.py,sha256=zDgm1kDrzLyCuIcNb8VXSdnb_CcVNjGkjgiIDVlsh74,3023
|
|
51
52
|
cmem_cmemc/smart_path/clients/__init__.py,sha256=YFOm69BfTCRvAcJjN_CoUmCv3kzEciyYOPUG337p_pA,1696
|
|
52
53
|
cmem_cmemc/smart_path/clients/http.py,sha256=3clZu2v4uuOvPY4MY_8SVSy7hIXJDNooahFRBRpy0ok,2347
|
|
53
|
-
cmem_cmemc/string_processor.py,sha256=
|
|
54
|
+
cmem_cmemc/string_processor.py,sha256=0EZVl3UeVgV5EEYGLPvCGHrJDMd0Ezekkwg6mDQVBZI,3112
|
|
54
55
|
cmem_cmemc/title_helper.py,sha256=7frjAR54_Xc1gszOWXfzSmKFTawNJQ7kkXhZcHmQLyw,1250
|
|
55
56
|
cmem_cmemc/utils.py,sha256=PkDFDISz7uemJCmyIWmtCcjfR_gRnRBL8ao76Ex-py8,14669
|
|
56
|
-
cmem_cmemc-25.
|
|
57
|
-
cmem_cmemc-25.
|
|
58
|
-
cmem_cmemc-25.
|
|
59
|
-
cmem_cmemc-25.
|
|
60
|
-
cmem_cmemc-25.
|
|
57
|
+
cmem_cmemc-25.5.0rc1.dist-info/METADATA,sha256=NmLZwGDtjnG7MHqkSozPvI85YUTWbt1KVnLUAJdeZFQ,5718
|
|
58
|
+
cmem_cmemc-25.5.0rc1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
59
|
+
cmem_cmemc-25.5.0rc1.dist-info/entry_points.txt,sha256=2G0AWAyz501EHpFTjIxccdlCTsHt80NT0pdUGP1QkPA,45
|
|
60
|
+
cmem_cmemc-25.5.0rc1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
61
|
+
cmem_cmemc-25.5.0rc1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|