cmem-cmemc 25.5.0rc1__py3-none-any.whl → 26.1.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/cli.py +11 -6
- cmem_cmemc/command.py +1 -1
- cmem_cmemc/command_group.py +59 -31
- cmem_cmemc/commands/acl.py +403 -26
- cmem_cmemc/commands/admin.py +10 -10
- cmem_cmemc/commands/client.py +12 -5
- cmem_cmemc/commands/config.py +106 -12
- cmem_cmemc/commands/dataset.py +163 -172
- cmem_cmemc/commands/file.py +509 -0
- cmem_cmemc/commands/graph.py +200 -72
- cmem_cmemc/commands/graph_imports.py +12 -5
- cmem_cmemc/commands/graph_insights.py +157 -53
- cmem_cmemc/commands/metrics.py +15 -9
- cmem_cmemc/commands/migration.py +12 -4
- cmem_cmemc/commands/package.py +548 -0
- cmem_cmemc/commands/project.py +157 -22
- cmem_cmemc/commands/python.py +9 -5
- cmem_cmemc/commands/query.py +119 -25
- cmem_cmemc/commands/scheduler.py +6 -4
- cmem_cmemc/commands/store.py +2 -1
- cmem_cmemc/commands/user.py +124 -24
- cmem_cmemc/commands/validation.py +15 -10
- cmem_cmemc/commands/variable.py +264 -61
- cmem_cmemc/commands/vocabulary.py +31 -17
- cmem_cmemc/commands/workflow.py +21 -11
- cmem_cmemc/completion.py +126 -109
- cmem_cmemc/context.py +40 -10
- cmem_cmemc/exceptions.py +8 -2
- cmem_cmemc/manual_helper/graph.py +2 -2
- cmem_cmemc/manual_helper/multi_page.py +5 -7
- cmem_cmemc/object_list.py +234 -7
- cmem_cmemc/placeholder.py +2 -2
- cmem_cmemc/string_processor.py +153 -4
- cmem_cmemc/title_helper.py +50 -0
- cmem_cmemc/utils.py +9 -8
- {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/METADATA +7 -6
- cmem_cmemc-26.1.0rc1.dist-info/RECORD +62 -0
- {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/WHEEL +1 -1
- cmem_cmemc/commands/resource.py +0 -220
- cmem_cmemc-25.5.0rc1.dist-info/RECORD +0 -61
- {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/entry_points.txt +0 -0
- {cmem_cmemc-25.5.0rc1.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/licenses/LICENSE +0 -0
cmem_cmemc/context.py
CHANGED
|
@@ -15,7 +15,9 @@ from shutil import which
|
|
|
15
15
|
import click
|
|
16
16
|
import cmem.cmempy.config as cmempy_config
|
|
17
17
|
import urllib3
|
|
18
|
+
from cmem.cmempy.config import get_cmem_base_uri
|
|
18
19
|
from cmem.cmempy.health import get_di_version, get_explore_version
|
|
20
|
+
from cmem_client.client import Client
|
|
19
21
|
from pygments import highlight
|
|
20
22
|
from pygments.formatters import get_formatter_by_name
|
|
21
23
|
from pygments.lexers import get_lexer_by_name
|
|
@@ -29,9 +31,9 @@ from cmem_cmemc.exceptions import InvalidConfigurationError
|
|
|
29
31
|
from cmem_cmemc.string_processor import StringProcessor, process_row
|
|
30
32
|
from cmem_cmemc.utils import is_enabled, str_to_bool
|
|
31
33
|
|
|
32
|
-
DI_TARGET_VERSION = "v25.
|
|
34
|
+
DI_TARGET_VERSION = "v25.3.0"
|
|
33
35
|
|
|
34
|
-
EXPLORE_TARGET_VERSION = "v25.
|
|
36
|
+
EXPLORE_TARGET_VERSION = "v25.3.0"
|
|
35
37
|
|
|
36
38
|
KNOWN_CONFIG_KEYS = {
|
|
37
39
|
"CMEM_BASE_URI": cmempy_config.get_cmem_base_uri,
|
|
@@ -55,6 +57,34 @@ KNOWN_SECRET_KEYS = ("OAUTH_PASSWORD", "OAUTH_CLIENT_SECRET", "OAUTH_ACCESS_TOKE
|
|
|
55
57
|
SSL_VERIFY_WARNING = "SSL verification is disabled (SSL_VERIFY=False)."
|
|
56
58
|
|
|
57
59
|
|
|
60
|
+
def build_caption(
|
|
61
|
+
count: int,
|
|
62
|
+
item_name: str,
|
|
63
|
+
instance: str | None = None,
|
|
64
|
+
filtered: bool = False,
|
|
65
|
+
plural: str | None = None,
|
|
66
|
+
) -> str:
|
|
67
|
+
"""Build a standardized caption for table outputs.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
----
|
|
71
|
+
count: Number of items in the table
|
|
72
|
+
item_name: Name of the items (e.g., "file", "project", "query")
|
|
73
|
+
instance: Instance URI (if None, uses get_cmem_base_uri())
|
|
74
|
+
filtered: Whether the results are filtered
|
|
75
|
+
plural: Optional Plural form of the item_name (e.g. "queries"), with giving
|
|
76
|
+
a plural form, simply a 's' is added for plural.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
-------
|
|
80
|
+
Caption string in format: "X thing(s) of INSTANCE (filtered)"
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
term = (plural if plural else f"{item_name}s") if count != 1 else item_name
|
|
84
|
+
base = f"{count} {term} of {instance or get_cmem_base_uri()}"
|
|
85
|
+
return f"{base} (filtered)" if filtered else base
|
|
86
|
+
|
|
87
|
+
|
|
58
88
|
class ApplicationContext:
|
|
59
89
|
"""Context of the command line interface."""
|
|
60
90
|
|
|
@@ -72,9 +102,6 @@ class ApplicationContext:
|
|
|
72
102
|
console: Console
|
|
73
103
|
console_width: int | None = None
|
|
74
104
|
|
|
75
|
-
# pylint: disable=too-many-instance-attributes
|
|
76
|
-
# pylint: disable=too-many-public-methods
|
|
77
|
-
|
|
78
105
|
def __init__(
|
|
79
106
|
self,
|
|
80
107
|
config_file: str,
|
|
@@ -93,6 +120,11 @@ class ApplicationContext:
|
|
|
93
120
|
self.console = Console(markup=True, emoji_variant="emoji")
|
|
94
121
|
self.update_console_width()
|
|
95
122
|
|
|
123
|
+
@property
|
|
124
|
+
def client(self) -> Client:
|
|
125
|
+
"""The cmem_client Client object."""
|
|
126
|
+
return Client.from_cmempy()
|
|
127
|
+
|
|
96
128
|
@staticmethod
|
|
97
129
|
def from_params(params: dict) -> "ApplicationContext":
|
|
98
130
|
"""Create an ApplicationContext instance from a dictionary.
|
|
@@ -273,9 +305,9 @@ class ApplicationContext:
|
|
|
273
305
|
return
|
|
274
306
|
if section_string not in self.get_config():
|
|
275
307
|
raise InvalidConfigurationError(
|
|
276
|
-
self,
|
|
277
308
|
f"There is no connection '{section_string}' configured in "
|
|
278
309
|
f"config '{self.config_file}'.",
|
|
310
|
+
app=self,
|
|
279
311
|
)
|
|
280
312
|
self.connection = section_string
|
|
281
313
|
|
|
@@ -294,10 +326,10 @@ class ApplicationContext:
|
|
|
294
326
|
config.read(self.get_config_file(), encoding="utf-8")
|
|
295
327
|
except configparser.Error as error:
|
|
296
328
|
raise InvalidConfigurationError(
|
|
297
|
-
self,
|
|
298
329
|
"The following config parser error needs to be fixed with your config file:\n"
|
|
299
330
|
f"{error!s}\n"
|
|
300
331
|
"You can use the 'config edit' command to fix this.",
|
|
332
|
+
app=self,
|
|
301
333
|
) from error
|
|
302
334
|
except Exception as error: # noqa: BLE001
|
|
303
335
|
self.echo_debug(f"Could not read config file - provide empty config: {error!s}")
|
|
@@ -337,7 +369,6 @@ class ApplicationContext:
|
|
|
337
369
|
2024-05-17: also allows list of strings now
|
|
338
370
|
2024-05-17: new prepend_line parameter
|
|
339
371
|
"""
|
|
340
|
-
# pylint: disable=invalid-name
|
|
341
372
|
click.echo("") if prepend_line is True else None
|
|
342
373
|
messages: list[str] = [message] if isinstance(message, str) else message
|
|
343
374
|
for _ in messages:
|
|
@@ -434,6 +465,7 @@ class ApplicationContext:
|
|
|
434
465
|
)
|
|
435
466
|
if caption is not None:
|
|
436
467
|
table.caption = caption
|
|
468
|
+
table.min_width = len(caption)
|
|
437
469
|
for header in headers:
|
|
438
470
|
table.add_column(header, overflow="fold")
|
|
439
471
|
for row_source in rows:
|
|
@@ -450,13 +482,11 @@ class ApplicationContext:
|
|
|
450
482
|
|
|
451
483
|
def echo_success(self, message: str, nl: bool = True, condition: bool = True) -> None:
|
|
452
484
|
"""Output success message, if not suppressed by --quiet."""
|
|
453
|
-
# pylint: disable=invalid-name
|
|
454
485
|
self.echo_info(message, fg="green", nl=nl, condition=condition)
|
|
455
486
|
|
|
456
487
|
@staticmethod
|
|
457
488
|
def echo_result(message: str, nl: bool = True) -> None:
|
|
458
489
|
"""Output result message, can NOT be suppressed by --quiet."""
|
|
459
|
-
# pylint: disable=invalid-name
|
|
460
490
|
click.echo(message, nl=nl)
|
|
461
491
|
|
|
462
492
|
def check_concrete_version(self, name: str, version: str, target_version: str) -> None:
|
cmem_cmemc/exceptions.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
+
from click.globals import get_current_context
|
|
6
|
+
|
|
5
7
|
if TYPE_CHECKING:
|
|
6
8
|
from cmem_cmemc.context import ApplicationContext
|
|
7
9
|
|
|
@@ -9,8 +11,12 @@ if TYPE_CHECKING:
|
|
|
9
11
|
class CmemcError(ValueError):
|
|
10
12
|
"""Base exception for CMEM-CMEMC-related errors."""
|
|
11
13
|
|
|
12
|
-
def __init__(self, app: "ApplicationContext"
|
|
13
|
-
super().__init__(
|
|
14
|
+
def __init__(self, message: str, app: "ApplicationContext | None" = None):
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
if app is None:
|
|
17
|
+
ctx = get_current_context(silent=True)
|
|
18
|
+
if ctx and hasattr(ctx, "obj"):
|
|
19
|
+
app = ctx.obj
|
|
14
20
|
self.app = app
|
|
15
21
|
|
|
16
22
|
|
|
@@ -56,7 +56,7 @@ def print_group_manual_graph_recursive(
|
|
|
56
56
|
ctx.obj.echo_info(f"{iri} a cli:CommandGroup .")
|
|
57
57
|
ctx.obj.echo_info(f"{iri} rdfs:label '{prefix}{key} Command Group' .")
|
|
58
58
|
ctx.obj.echo_info(f"{iri} cli:subGroupOf {sub_group_iri} .")
|
|
59
|
-
ctx.obj.echo_info(f
|
|
59
|
+
ctx.obj.echo_info(f'{iri} rdfs:comment """{comment}""" .')
|
|
60
60
|
print_group_manual_graph_recursive(item, ctx=ctx, prefix=f"{prefix}{key}-")
|
|
61
61
|
elif isinstance(item, click.Command):
|
|
62
62
|
comment = item.get_short_help_str(limit=200)
|
|
@@ -64,7 +64,7 @@ def print_group_manual_graph_recursive(
|
|
|
64
64
|
ctx.obj.echo_info(f"{iri} a cli:Command .")
|
|
65
65
|
ctx.obj.echo_info(f"{iri} rdfs:label '{prefix}{key} Command' .")
|
|
66
66
|
ctx.obj.echo_info(f"{iri} cli:group {group_iri} .")
|
|
67
|
-
ctx.obj.echo_info(f
|
|
67
|
+
ctx.obj.echo_info(f'{iri} rdfs:comment """{comment}""" .')
|
|
68
68
|
for parameter in item.params:
|
|
69
69
|
print_parameter_manual_graph(parameter, ctx=ctx, prefix=f"{prefix}{key}-")
|
|
70
70
|
else:
|
|
@@ -26,12 +26,13 @@ def get_icon_for_command_group(full_name: str) -> str:
|
|
|
26
26
|
"admin workspace python": "material/language-python",
|
|
27
27
|
"config": "material/cog-outline",
|
|
28
28
|
"dataset": "eccenca/artefact-dataset",
|
|
29
|
-
"dataset resource": "eccenca/artefact-file",
|
|
30
29
|
"project": "eccenca/artefact-project",
|
|
30
|
+
"project file": "eccenca/artefact-file",
|
|
31
31
|
"project variable": "material/variable-box",
|
|
32
32
|
"query": "eccenca/application-queries",
|
|
33
33
|
"graph": "eccenca/artefact-dataset-eccencadataplatform",
|
|
34
34
|
"graph imports": "material/family-tree",
|
|
35
|
+
"graph insights": "eccenca/graph-insights",
|
|
35
36
|
"graph validation": "octicons/verified-16",
|
|
36
37
|
"vocabulary": "eccenca/application-vocabularies",
|
|
37
38
|
"vocabulary cache": "eccenca/application-vocabularies",
|
|
@@ -42,7 +43,6 @@ def get_icon_for_command_group(full_name: str) -> str:
|
|
|
42
43
|
|
|
43
44
|
def get_tags_for_command_group(full_name: str) -> str:
|
|
44
45
|
"""Get list of tags for a command group name as markdown head section."""
|
|
45
|
-
# pylint: disable=consider-using-join
|
|
46
46
|
tags = {
|
|
47
47
|
"admin": ["cmemc"],
|
|
48
48
|
"admin metrics": ["cmemc"],
|
|
@@ -54,8 +54,8 @@ def get_tags_for_command_group(full_name: str) -> str:
|
|
|
54
54
|
"admin workspace python": ["Python", "cmemc"],
|
|
55
55
|
"config": ["Configuration", "cmemc"],
|
|
56
56
|
"dataset": ["cmemc"],
|
|
57
|
-
"dataset resource": ["cmemc"],
|
|
58
57
|
"project": ["Project", "cmemc"],
|
|
58
|
+
"project file": ["Files", "cmemc"],
|
|
59
59
|
"project variable": ["Variables", "cmemc"],
|
|
60
60
|
"query": ["SPARQL", "cmemc"],
|
|
61
61
|
"graph": ["KnowledgeGraph", "cmemc"],
|
|
@@ -145,12 +145,10 @@ def get_commands_for_table_recursive(
|
|
|
145
145
|
if isinstance(item, Command):
|
|
146
146
|
command_name = item.name
|
|
147
147
|
group_link = f"{prefix}/{group_name}/index.md".replace(" ", "/")
|
|
148
|
-
|
|
149
|
-
group_link = group_link[1:]
|
|
148
|
+
group_link = group_link.removeprefix("/")
|
|
150
149
|
command_anchor = f"{prefix}-{group_name}".replace(" ", "-")
|
|
151
150
|
command_anchor += f"-{command_name}"
|
|
152
|
-
|
|
153
|
-
command_anchor = command_anchor[1:]
|
|
151
|
+
command_anchor = command_anchor.removeprefix("-")
|
|
154
152
|
command_link = f"{group_link}#{command_anchor}"
|
|
155
153
|
new_command = {
|
|
156
154
|
"command_name": command_name,
|
cmem_cmemc/object_list.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Filterable object list."""
|
|
1
|
+
"""Filterable object list with enhanced filter types."""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
@@ -9,7 +9,11 @@ from typing import Literal
|
|
|
9
9
|
from click import Argument, Context, UsageError
|
|
10
10
|
from click.shell_completion import CompletionItem
|
|
11
11
|
|
|
12
|
-
from cmem_cmemc.completion import
|
|
12
|
+
from cmem_cmemc.completion import (
|
|
13
|
+
finalize_completion,
|
|
14
|
+
get_completion_args,
|
|
15
|
+
suppress_completion_errors,
|
|
16
|
+
)
|
|
13
17
|
from cmem_cmemc.context import ApplicationContext
|
|
14
18
|
from cmem_cmemc.title_helper import TitleHelper
|
|
15
19
|
|
|
@@ -19,6 +23,7 @@ class Filter(ABC):
|
|
|
19
23
|
|
|
20
24
|
name: str
|
|
21
25
|
description: str
|
|
26
|
+
hidden: bool = False # If True, filter is hidden from help text and shell completion
|
|
22
27
|
|
|
23
28
|
@abstractmethod
|
|
24
29
|
def is_filtered(self, object_: dict, value: str) -> bool:
|
|
@@ -82,6 +87,16 @@ def transform_lower(ctx: Filter, value: str) -> str: # noqa: ARG001
|
|
|
82
87
|
return value.lower()
|
|
83
88
|
|
|
84
89
|
|
|
90
|
+
def transform_list_none(ctx: Filter, value: list) -> list: # noqa: ARG001
|
|
91
|
+
"""Transform: do nothing"""
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def transform_extract_labels(ctx: Filter, value: list) -> list: # noqa: ARG001
|
|
96
|
+
"""Transform: extract label values from list of dictionaries"""
|
|
97
|
+
return [item["label"] for item in value if isinstance(item, dict) and "label" in item]
|
|
98
|
+
|
|
99
|
+
|
|
85
100
|
class DirectValuePropertyFilter(Filter):
|
|
86
101
|
"""Class to create a filter based on direct properties of an object.
|
|
87
102
|
|
|
@@ -202,6 +217,118 @@ class DirectValuePropertyFilter(Filter):
|
|
|
202
217
|
raise NotImplementedError(f"Completion method {self.completion_method} not implemented.")
|
|
203
218
|
|
|
204
219
|
|
|
220
|
+
class MultiFieldPropertyFilter(Filter):
|
|
221
|
+
"""Filter that searches across multiple object fields with a single value.
|
|
222
|
+
|
|
223
|
+
This filter can search fields either:
|
|
224
|
+
1. Individually (match_mode="any") - matches if ANY field matches
|
|
225
|
+
2. Concatenated (match_mode="concat") - matches against all fields joined together
|
|
226
|
+
|
|
227
|
+
The "any" mode is more flexible and typically what users expect - e.g.,
|
|
228
|
+
searching "workflow" will match if it appears in name OR label OR description.
|
|
229
|
+
|
|
230
|
+
The "concat" mode is useful when you need to match patterns that span fields,
|
|
231
|
+
though this is less common in practice.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
# Search for regex in ANY of: name, label, or description
|
|
235
|
+
filter = MultiFieldPropertyFilter(
|
|
236
|
+
name="regex",
|
|
237
|
+
description="Search in name, label, or description",
|
|
238
|
+
property_keys=["name", "label", "description"],
|
|
239
|
+
compare=compare_regex,
|
|
240
|
+
match_mode="any" # This is the default
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
name: str
|
|
246
|
+
description: str
|
|
247
|
+
property_keys: list[str]
|
|
248
|
+
separator: str
|
|
249
|
+
compare: Callable[[Filter, str, str], bool]
|
|
250
|
+
match_mode: Literal["any", "concat"]
|
|
251
|
+
fixed_completion: list[CompletionItem]
|
|
252
|
+
fixed_completion_only: bool
|
|
253
|
+
|
|
254
|
+
def __init__( # noqa: PLR0913
|
|
255
|
+
self,
|
|
256
|
+
name: str,
|
|
257
|
+
description: str,
|
|
258
|
+
property_keys: list[str],
|
|
259
|
+
separator: str = " ",
|
|
260
|
+
compare: Callable[[Filter, str, str], bool] = compare_regex,
|
|
261
|
+
match_mode: Literal["any", "concat"] = "any",
|
|
262
|
+
fixed_completion: list[CompletionItem] | None = None,
|
|
263
|
+
fixed_completion_only: bool = False,
|
|
264
|
+
):
|
|
265
|
+
"""Create a multi-field filter.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
name: The name of the filter
|
|
269
|
+
description: The description of the filter
|
|
270
|
+
property_keys: List of property keys to search across
|
|
271
|
+
separator: String to use when joining field values (default: space)
|
|
272
|
+
compare: Comparison function (default: case-insensitive regex)
|
|
273
|
+
match_mode: "any" to match if ANY field matches (OR logic),
|
|
274
|
+
"concat" to match against concatenated fields
|
|
275
|
+
fixed_completion: A fixed list of CompletionItem objects used for value completion
|
|
276
|
+
fixed_completion_only: Raise an UsageError if a value is not from fixed completion
|
|
277
|
+
|
|
278
|
+
"""
|
|
279
|
+
self.name = name
|
|
280
|
+
self.description = description
|
|
281
|
+
self.property_keys = property_keys
|
|
282
|
+
self.separator = separator
|
|
283
|
+
self.compare = compare
|
|
284
|
+
self.match_mode = match_mode
|
|
285
|
+
if fixed_completion is not None:
|
|
286
|
+
self.fixed_completion = fixed_completion
|
|
287
|
+
else:
|
|
288
|
+
self.fixed_completion = []
|
|
289
|
+
self.fixed_completion_only = fixed_completion_only
|
|
290
|
+
|
|
291
|
+
def is_filtered(self, object_: dict, value: str) -> bool:
|
|
292
|
+
"""Return True if comparison matches based on match_mode.
|
|
293
|
+
|
|
294
|
+
- "any" mode: Returns True if value matches ANY field (OR logic)
|
|
295
|
+
- "concat" mode: Returns True if value matches concatenated fields
|
|
296
|
+
"""
|
|
297
|
+
# Validate fixed_completion_only constraint
|
|
298
|
+
fixed_values = [_.value for _ in self.fixed_completion]
|
|
299
|
+
if self.fixed_completion_only and value not in fixed_values:
|
|
300
|
+
raise UsageError(
|
|
301
|
+
f"'{value}' is not a correct filter value for filter '{self.name}'. "
|
|
302
|
+
f"Use one of {', '.join(fixed_values)}."
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Collect all field values
|
|
306
|
+
field_values = [
|
|
307
|
+
str(object_[key])
|
|
308
|
+
for key in self.property_keys
|
|
309
|
+
if key in object_ and object_[key] is not None
|
|
310
|
+
]
|
|
311
|
+
if not field_values:
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
if self.match_mode == "any":
|
|
315
|
+
# Match if ANY individual field matches (OR logic)
|
|
316
|
+
return any(self.compare(self, field_value, value) for field_value in field_values)
|
|
317
|
+
# Match against concatenated string
|
|
318
|
+
combined_value = self.separator.join(field_values)
|
|
319
|
+
return bool(self.compare(self, combined_value, value))
|
|
320
|
+
|
|
321
|
+
def complete_values(self, objects: list[dict], incomplete: str) -> list[CompletionItem]:
|
|
322
|
+
"""Provide completion items for filter values.
|
|
323
|
+
|
|
324
|
+
Returns fixed_completion if available, otherwise no completion.
|
|
325
|
+
"""
|
|
326
|
+
_ = objects, incomplete
|
|
327
|
+
if self.fixed_completion:
|
|
328
|
+
return self.fixed_completion
|
|
329
|
+
return []
|
|
330
|
+
|
|
331
|
+
|
|
205
332
|
class DirectListPropertyFilter(Filter):
|
|
206
333
|
"""Class to create filter based on direct list properties of an object
|
|
207
334
|
|
|
@@ -213,6 +340,7 @@ class DirectListPropertyFilter(Filter):
|
|
|
213
340
|
description: str
|
|
214
341
|
property_key: str
|
|
215
342
|
title_helper: TitleHelper | None
|
|
343
|
+
transform: Callable[[Filter, list], list]
|
|
216
344
|
|
|
217
345
|
def __init__(
|
|
218
346
|
self,
|
|
@@ -220,6 +348,7 @@ class DirectListPropertyFilter(Filter):
|
|
|
220
348
|
description: str,
|
|
221
349
|
property_key: str,
|
|
222
350
|
title_helper: TitleHelper | None = None,
|
|
351
|
+
transform: Callable[[Filter, list], list] = transform_list_none,
|
|
223
352
|
):
|
|
224
353
|
"""Create the new filter
|
|
225
354
|
|
|
@@ -232,11 +361,15 @@ class DirectListPropertyFilter(Filter):
|
|
|
232
361
|
title_helper:
|
|
233
362
|
(Optional) TitleHelper instance which will be used to provide
|
|
234
363
|
resource titles as descriptions of completions candidates
|
|
364
|
+
transform:
|
|
365
|
+
(Optional) Transform function to apply to the list before filtering/completion.
|
|
366
|
+
Receives the filter instance and the list, returns a transformed list.
|
|
235
367
|
"""
|
|
236
368
|
self.name = name
|
|
237
369
|
self.description = description
|
|
238
370
|
self.property_key = property_key
|
|
239
371
|
self.title_helper = title_helper
|
|
372
|
+
self.transform = transform
|
|
240
373
|
|
|
241
374
|
def is_filtered(self, object_: dict, value: str) -> bool:
|
|
242
375
|
"""Return True if the object is filtered (stays in list)."""
|
|
@@ -246,7 +379,8 @@ class DirectListPropertyFilter(Filter):
|
|
|
246
379
|
return False # key value is None
|
|
247
380
|
if not isinstance(object_[self.property_key], list):
|
|
248
381
|
return False # key value is not a list
|
|
249
|
-
|
|
382
|
+
transformed_list = self.transform(self, object_[self.property_key])
|
|
383
|
+
return value in [str(_) for _ in transformed_list]
|
|
250
384
|
|
|
251
385
|
def complete_values(self, objects: list[dict], incomplete: str) -> list[CompletionItem]:
|
|
252
386
|
"""Provide completion items for filter values"""
|
|
@@ -258,13 +392,100 @@ class DirectListPropertyFilter(Filter):
|
|
|
258
392
|
continue # key value is None
|
|
259
393
|
if not isinstance(object_[self.property_key], list):
|
|
260
394
|
continue # key value is not a list
|
|
261
|
-
|
|
395
|
+
transformed_list = self.transform(self, object_[self.property_key])
|
|
396
|
+
candidates.extend([str(_) for _ in transformed_list])
|
|
262
397
|
if self.title_helper:
|
|
263
398
|
self.title_helper.get(list(set(candidates)))
|
|
264
399
|
candidates = [(_, self.title_helper.get(_)) for _ in candidates]
|
|
265
400
|
return finalize_completion(candidates=candidates, incomplete=incomplete)
|
|
266
401
|
|
|
267
402
|
|
|
403
|
+
class DirectMultiValuePropertyFilter(Filter):
|
|
404
|
+
"""Filter that matches when object property equals ANY value in a delimiter-separated string.
|
|
405
|
+
|
|
406
|
+
This filter accepts a single string containing multiple values separated by a delimiter
|
|
407
|
+
(comma by default) and returns True if the object's property matches any of them.
|
|
408
|
+
Typically used as a hidden internal filter for multi-ID operations.
|
|
409
|
+
|
|
410
|
+
Missing keys or Null values are treated as no match.
|
|
411
|
+
|
|
412
|
+
Example:
|
|
413
|
+
filter = DirectMultiValuePropertyFilter("ids", "IDs filter", "iri", delimiter=",")
|
|
414
|
+
# Pass comma-separated values: "id1,id2,id3"
|
|
415
|
+
# Matches objects where iri equals "id1" OR "id2" OR "id3"
|
|
416
|
+
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
name: str
|
|
420
|
+
description: str
|
|
421
|
+
property_key: str
|
|
422
|
+
transform: Callable[[Filter, str], str]
|
|
423
|
+
hidden: bool = True
|
|
424
|
+
|
|
425
|
+
def __init__(
|
|
426
|
+
self,
|
|
427
|
+
name: str,
|
|
428
|
+
description: str,
|
|
429
|
+
property_key: str,
|
|
430
|
+
transform: Callable[[Filter, str], str] = transform_none,
|
|
431
|
+
delimiter: str = ",",
|
|
432
|
+
):
|
|
433
|
+
"""Create a new multi-value filter.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
name: The name of the filter (used as identifier)
|
|
437
|
+
description: The description of the filter (used in filter name completion)
|
|
438
|
+
property_key: The key of the property which is compared
|
|
439
|
+
transform: Optional value transformation function (e.g., to lowercase)
|
|
440
|
+
delimiter: String delimiter for splitting multiple values (default: ",")
|
|
441
|
+
|
|
442
|
+
"""
|
|
443
|
+
self.name = name
|
|
444
|
+
self.description = description
|
|
445
|
+
self.property_key = property_key
|
|
446
|
+
self.transform = transform
|
|
447
|
+
self.delimiter = delimiter
|
|
448
|
+
|
|
449
|
+
def is_filtered(self, object_: dict, value: str) -> bool:
|
|
450
|
+
"""Return True if object property matches ANY value in delimiter-separated string.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
object_: The object to filter
|
|
454
|
+
value: Delimiter-separated string of values (e.g., "val1,val2,val3")
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
True if object's property value matches any of the provided values (OR logic)
|
|
458
|
+
|
|
459
|
+
"""
|
|
460
|
+
if self.property_key not in object_ or object_[self.property_key] is None:
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
input_values = value.split(self.delimiter)
|
|
464
|
+
|
|
465
|
+
obj_val = str(object_[self.property_key])
|
|
466
|
+
|
|
467
|
+
input_values = [v.strip() for v in input_values]
|
|
468
|
+
|
|
469
|
+
return obj_val in input_values
|
|
470
|
+
|
|
471
|
+
def complete_values(self, objects: list[dict], incomplete: str) -> list[CompletionItem]:
|
|
472
|
+
"""Provide completion items for filter values.
|
|
473
|
+
|
|
474
|
+
Note: Hidden filters return no completions.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
objects: List of objects (unused for hidden filters)
|
|
478
|
+
incomplete: Incomplete value being typed (unused for hidden filters)
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
Empty list (hidden filters provide no completion)
|
|
482
|
+
|
|
483
|
+
"""
|
|
484
|
+
# Hidden filters provide no completion
|
|
485
|
+
_ = objects, incomplete
|
|
486
|
+
return []
|
|
487
|
+
|
|
488
|
+
|
|
268
489
|
class ObjectList:
|
|
269
490
|
"""Filterable object list"""
|
|
270
491
|
|
|
@@ -303,10 +524,14 @@ class ObjectList:
|
|
|
303
524
|
self.filters = {}
|
|
304
525
|
|
|
305
526
|
def get_filter_help_text(self) -> str:
|
|
306
|
-
"""Get help text for the filter option.
|
|
527
|
+
"""Get help text for the filter option.
|
|
528
|
+
|
|
529
|
+
Hidden filters are excluded from the help text.
|
|
530
|
+
"""
|
|
531
|
+
visible_filters = [name for name in self.filters if not self.filters[name].hidden]
|
|
307
532
|
return (
|
|
308
533
|
f"Filter {self.name} by one of the following filter names and a "
|
|
309
|
-
f"corresponding value: {', '.join(
|
|
534
|
+
f"corresponding value: {', '.join(visible_filters)}."
|
|
310
535
|
)
|
|
311
536
|
|
|
312
537
|
def get_filter_names(self) -> list[str]:
|
|
@@ -336,6 +561,7 @@ class ObjectList:
|
|
|
336
561
|
filtered = the_filter.filter_list(value=filter_value, objects=filtered)
|
|
337
562
|
return filtered
|
|
338
563
|
|
|
564
|
+
@suppress_completion_errors
|
|
339
565
|
def complete_values(
|
|
340
566
|
self,
|
|
341
567
|
ctx: Context,
|
|
@@ -350,11 +576,12 @@ class ObjectList:
|
|
|
350
576
|
last_argument = args[len(args) - 1]
|
|
351
577
|
|
|
352
578
|
if last_argument == "--filter":
|
|
353
|
-
# complete filter names and descriptions
|
|
579
|
+
# complete filter names and descriptions (hide hidden filters)
|
|
354
580
|
candidates = [
|
|
355
581
|
(name, self.get_filter(name).description)
|
|
356
582
|
for name in self.get_filter_names()
|
|
357
583
|
if name not in previous_filter_names # do not show already used filters
|
|
584
|
+
and not self.get_filter(name).hidden # hide hidden filters
|
|
358
585
|
]
|
|
359
586
|
return finalize_completion(
|
|
360
587
|
candidates=candidates,
|
cmem_cmemc/placeholder.py
CHANGED
|
@@ -36,7 +36,7 @@ class QueryPlaceholder:
|
|
|
36
36
|
"""Prepare a list of placeholder values"""
|
|
37
37
|
result = self.value_query.get_json_results()
|
|
38
38
|
projection_vars = result["head"]["vars"]
|
|
39
|
-
bindings = result["results"]["bindings"]
|
|
39
|
+
bindings: list[dict] = result["results"]["bindings"]
|
|
40
40
|
if "value" not in projection_vars:
|
|
41
41
|
return []
|
|
42
42
|
|
|
@@ -50,7 +50,7 @@ class QueryPlaceholder:
|
|
|
50
50
|
values_with_description = []
|
|
51
51
|
for _ in bindings:
|
|
52
52
|
value = str(_["value"]["value"])
|
|
53
|
-
description =
|
|
53
|
+
description = _.get("description", {}).get("value", "")
|
|
54
54
|
values_with_description.append((value, description))
|
|
55
55
|
return values_with_description
|
|
56
56
|
|