cmem-cmemc 26.1.0rc2__py3-none-any.whl → 26.1.0rc4__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 +17 -2
- cmem_cmemc/commands/acl.py +17 -48
- cmem_cmemc/commands/dataset.py +33 -58
- cmem_cmemc/commands/file.py +16 -45
- cmem_cmemc/commands/graph.py +13 -45
- cmem_cmemc/commands/graph_insights.py +18 -14
- cmem_cmemc/commands/package.py +70 -1
- cmem_cmemc/commands/project.py +17 -12
- cmem_cmemc/commands/user.py +13 -49
- cmem_cmemc/commands/variable.py +15 -39
- cmem_cmemc/context.py +42 -2
- cmem_cmemc/utils.py +113 -0
- {cmem_cmemc-26.1.0rc2.dist-info → cmem_cmemc-26.1.0rc4.dist-info}/METADATA +2 -2
- {cmem_cmemc-26.1.0rc2.dist-info → cmem_cmemc-26.1.0rc4.dist-info}/RECORD +17 -17
- {cmem_cmemc-26.1.0rc2.dist-info → cmem_cmemc-26.1.0rc4.dist-info}/WHEEL +1 -1
- {cmem_cmemc-26.1.0rc2.dist-info → cmem_cmemc-26.1.0rc4.dist-info}/entry_points.txt +0 -0
- {cmem_cmemc-26.1.0rc2.dist-info → cmem_cmemc-26.1.0rc4.dist-info}/licenses/LICENSE +0 -0
cmem_cmemc/cli.py
CHANGED
|
@@ -68,7 +68,21 @@ CONTEXT_SETTINGS = {"auto_envvar_prefix": "CMEMC", "help_option_names": ["-h", "
|
|
|
68
68
|
)
|
|
69
69
|
@click.option("-q", "--quiet", is_flag=True, help="Suppress any non-error info messages.")
|
|
70
70
|
@click.option(
|
|
71
|
-
"-d",
|
|
71
|
+
"-d",
|
|
72
|
+
"--debug",
|
|
73
|
+
is_flag=True,
|
|
74
|
+
help="Output debug messages and stack traces after errors. The log level "
|
|
75
|
+
"can be set in the config via the CMEMC_LOG_LEVEL environment variable. "
|
|
76
|
+
"Options are: 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'"
|
|
77
|
+
" [default: DEBUG]",
|
|
78
|
+
)
|
|
79
|
+
@click.option(
|
|
80
|
+
"--log-level",
|
|
81
|
+
type=click.Choice(
|
|
82
|
+
["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], case_sensitive=False
|
|
83
|
+
),
|
|
84
|
+
help="Set the log level when --debug is enabled. "
|
|
85
|
+
"Can also be set via CMEMC_LOG_LEVEL environment variable.",
|
|
72
86
|
)
|
|
73
87
|
@click.option(
|
|
74
88
|
"--external-http-timeout",
|
|
@@ -89,6 +103,7 @@ def cli( # noqa: PLR0913
|
|
|
89
103
|
config_file: str,
|
|
90
104
|
connection: str,
|
|
91
105
|
external_http_timeout: int,
|
|
106
|
+
log_level: str,
|
|
92
107
|
) -> None:
|
|
93
108
|
"""Eccenca Corporate Memory Control (cmemc).
|
|
94
109
|
|
|
@@ -111,7 +126,7 @@ def cli( # noqa: PLR0913
|
|
|
111
126
|
|
|
112
127
|
cmemc is © 2026 eccenca GmbH, licensed under the Apache License 2.0.
|
|
113
128
|
"""
|
|
114
|
-
_ = connection, debug, quiet, config_file, external_http_timeout
|
|
129
|
+
_ = connection, debug, quiet, config_file, external_http_timeout, log_level
|
|
115
130
|
if " ".join(sys.argv).find("config edit") != -1:
|
|
116
131
|
app = ApplicationContext(config_file=config_file, debug=debug, quiet=quiet)
|
|
117
132
|
else:
|
cmem_cmemc/commands/acl.py
CHANGED
|
@@ -33,6 +33,7 @@ from cmem_cmemc.smart_path import SmartPath as Path
|
|
|
33
33
|
from cmem_cmemc.utils import (
|
|
34
34
|
convert_iri_to_qname,
|
|
35
35
|
convert_qname_to_iri,
|
|
36
|
+
get_objects_to_delete,
|
|
36
37
|
get_query_text,
|
|
37
38
|
struct_to_table,
|
|
38
39
|
)
|
|
@@ -172,53 +173,6 @@ acl_list = ObjectList(
|
|
|
172
173
|
)
|
|
173
174
|
|
|
174
175
|
|
|
175
|
-
def _validate_acl_ids(access_condition_ids: tuple[str, ...]) -> None:
|
|
176
|
-
"""Validate that all provided access condition IDs exist."""
|
|
177
|
-
if not access_condition_ids:
|
|
178
|
-
return
|
|
179
|
-
all_acls = fetch_all_acls()
|
|
180
|
-
all_iris = {acl["iri"] for acl in all_acls}
|
|
181
|
-
for acl_id in access_condition_ids:
|
|
182
|
-
iri = convert_qname_to_iri(qname=acl_id, default_ns=NS_ACL)
|
|
183
|
-
if iri not in all_iris:
|
|
184
|
-
raise click.ClickException(
|
|
185
|
-
f"Access condition {acl_id} not available. Use the 'admin acl list' "
|
|
186
|
-
"command to get a list of existing access conditions."
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def _get_acls_to_delete(
|
|
191
|
-
ctx: Context,
|
|
192
|
-
access_condition_ids: tuple[str, ...],
|
|
193
|
-
all_: bool,
|
|
194
|
-
filter_: tuple[tuple[str, str], ...],
|
|
195
|
-
) -> list[dict]:
|
|
196
|
-
"""Get the list of access conditions to delete based on selection method."""
|
|
197
|
-
if all_:
|
|
198
|
-
# Get all access conditions
|
|
199
|
-
return fetch_all_acls() # type: ignore[no-any-return]
|
|
200
|
-
|
|
201
|
-
# Validate provided IDs exist before proceeding
|
|
202
|
-
_validate_acl_ids(access_condition_ids)
|
|
203
|
-
|
|
204
|
-
# Build filter list
|
|
205
|
-
filter_to_apply = list(filter_) if filter_ else []
|
|
206
|
-
|
|
207
|
-
# Add IDs if provided (using internal multi-value filter)
|
|
208
|
-
if access_condition_ids:
|
|
209
|
-
iris = [convert_qname_to_iri(qname=_, default_ns=NS_ACL) for _ in access_condition_ids]
|
|
210
|
-
filter_to_apply.append(("ids", ",".join(iris)))
|
|
211
|
-
|
|
212
|
-
# Apply filters
|
|
213
|
-
acls = acl_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
|
|
214
|
-
|
|
215
|
-
# Validation: ensure we found access conditions
|
|
216
|
-
if not acls and not access_condition_ids:
|
|
217
|
-
raise click.ClickException("No access conditions found matching the provided filters.")
|
|
218
|
-
|
|
219
|
-
return acls
|
|
220
|
-
|
|
221
|
-
|
|
222
176
|
@click.command(cls=CmemcCommand, name="list")
|
|
223
177
|
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
224
178
|
@click.option(
|
|
@@ -645,8 +599,23 @@ def delete_command(
|
|
|
645
599
|
"Either specify access condition IDs OR use a --filter or the --all option."
|
|
646
600
|
)
|
|
647
601
|
|
|
602
|
+
# Convert qnames to IRIs for filtering
|
|
603
|
+
iris = [convert_qname_to_iri(qname=_, default_ns=NS_ACL) for _ in access_condition_ids]
|
|
604
|
+
|
|
648
605
|
# Get access conditions to delete based on selection method
|
|
649
|
-
acls_to_delete =
|
|
606
|
+
acls_to_delete = get_objects_to_delete(
|
|
607
|
+
ctx=ctx,
|
|
608
|
+
ids=tuple(iris),
|
|
609
|
+
all_=all_,
|
|
610
|
+
filter_=filter_,
|
|
611
|
+
object_list=acl_list,
|
|
612
|
+
get_all_objects=fetch_all_acls,
|
|
613
|
+
extract_id=lambda x: x["iri"],
|
|
614
|
+
error_message_template=(
|
|
615
|
+
"Access condition {} not available. "
|
|
616
|
+
"Use the 'admin acl list' command to get a list of existing access conditions."
|
|
617
|
+
),
|
|
618
|
+
)
|
|
650
619
|
|
|
651
620
|
# Avoid double removal as well as sort IRIs
|
|
652
621
|
iris_to_delete = sorted({acl["iri"] for acl in acls_to_delete}, key=lambda v: v.lower())
|
cmem_cmemc/commands/dataset.py
CHANGED
|
@@ -38,18 +38,26 @@ from cmem_cmemc.parameter_types.path import ClickSmartPath
|
|
|
38
38
|
from cmem_cmemc.smart_path import SmartPath as Path
|
|
39
39
|
from cmem_cmemc.string_processor import DatasetLink, DatasetTypeLink
|
|
40
40
|
from cmem_cmemc.title_helper import DatasetTypeTitleHelper, ProjectTitleHelper
|
|
41
|
-
from cmem_cmemc.utils import check_or_select_project, struct_to_table
|
|
41
|
+
from cmem_cmemc.utils import check_or_select_project, get_objects_to_delete, struct_to_table
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def
|
|
45
|
-
"""Get datasets for object list."""
|
|
44
|
+
def get_datasets_for_list(ctx: Context) -> list[dict]:
|
|
45
|
+
"""Get datasets for object list, transforming structure for filtering."""
|
|
46
46
|
_ = ctx
|
|
47
|
-
|
|
47
|
+
# Transform the dataset structure to add combined ID for easier filtering
|
|
48
|
+
transformed = []
|
|
49
|
+
for dataset in list_items(item_type="dataset")["results"]:
|
|
50
|
+
transformed_dataset = {
|
|
51
|
+
**dataset, # Keep all original fields
|
|
52
|
+
"combinedId": f"{dataset['projectId']}:{dataset['id']}", # Add combined ID
|
|
53
|
+
}
|
|
54
|
+
transformed.append(transformed_dataset)
|
|
55
|
+
return transformed
|
|
48
56
|
|
|
49
57
|
|
|
50
58
|
dataset_list = ObjectList(
|
|
51
59
|
name="datasets",
|
|
52
|
-
get_objects=
|
|
60
|
+
get_objects=get_datasets_for_list,
|
|
53
61
|
filters=[
|
|
54
62
|
DirectValuePropertyFilter(
|
|
55
63
|
name="project",
|
|
@@ -84,6 +92,11 @@ dataset_list = ObjectList(
|
|
|
84
92
|
description="Internal filter for multiple dataset IDs.",
|
|
85
93
|
property_key="id",
|
|
86
94
|
),
|
|
95
|
+
DirectMultiValuePropertyFilter(
|
|
96
|
+
name="combinedIds",
|
|
97
|
+
description="Internal filter for multiple combined dataset IDs (projectId:datasetId).",
|
|
98
|
+
property_key="combinedId",
|
|
99
|
+
),
|
|
87
100
|
],
|
|
88
101
|
)
|
|
89
102
|
|
|
@@ -111,57 +124,6 @@ def _validate_and_split_dataset_id(dataset_id: str) -> tuple[str, str]:
|
|
|
111
124
|
return project_part, dataset_part
|
|
112
125
|
|
|
113
126
|
|
|
114
|
-
def _validate_dataset_ids(dataset_ids: tuple[str, ...]) -> None:
|
|
115
|
-
"""Validate that all provided dataset IDs exist."""
|
|
116
|
-
if not dataset_ids:
|
|
117
|
-
return
|
|
118
|
-
all_datasets = list_items(item_type="dataset")["results"]
|
|
119
|
-
all_dataset_ids = [_["projectId"] + ":" + _["id"] for _ in all_datasets]
|
|
120
|
-
for dataset_id in dataset_ids:
|
|
121
|
-
if dataset_id not in all_dataset_ids:
|
|
122
|
-
raise CmemcError(
|
|
123
|
-
f"Dataset {dataset_id} not available. Use the 'dataset list' "
|
|
124
|
-
"command to get a list of existing datasets."
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _get_datasets_to_delete(
|
|
129
|
-
ctx: Context,
|
|
130
|
-
dataset_ids: tuple[str, ...],
|
|
131
|
-
all_: bool,
|
|
132
|
-
filter_: tuple[tuple[str, str], ...],
|
|
133
|
-
) -> list[str]:
|
|
134
|
-
"""Get the list of dataset IDs to delete based on selection method."""
|
|
135
|
-
if all_:
|
|
136
|
-
# Get all datasets
|
|
137
|
-
datasets = list_items(item_type="dataset")["results"]
|
|
138
|
-
return [_["projectId"] + ":" + _["id"] for _ in datasets]
|
|
139
|
-
|
|
140
|
-
# Validate provided IDs exist before proceeding
|
|
141
|
-
_validate_dataset_ids(dataset_ids)
|
|
142
|
-
|
|
143
|
-
# Build filter list
|
|
144
|
-
filter_to_apply = list(filter_) if filter_ else []
|
|
145
|
-
|
|
146
|
-
# Add IDs if provided (using internal multi-value filter)
|
|
147
|
-
if dataset_ids:
|
|
148
|
-
# Extract just the dataset ID part (after the colon) for filtering
|
|
149
|
-
dataset_id_parts = [_.split(":")[1] for _ in dataset_ids]
|
|
150
|
-
filter_to_apply.append(("ids", ",".join(dataset_id_parts)))
|
|
151
|
-
|
|
152
|
-
# Apply filters
|
|
153
|
-
datasets = dataset_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
|
|
154
|
-
|
|
155
|
-
# Build full dataset IDs
|
|
156
|
-
result = [_["projectId"] + ":" + _["id"] for _ in datasets]
|
|
157
|
-
|
|
158
|
-
# Validation: ensure we found datasets
|
|
159
|
-
if not result and not dataset_ids:
|
|
160
|
-
raise CmemcError("No datasets found matching the provided filters.")
|
|
161
|
-
|
|
162
|
-
return result
|
|
163
|
-
|
|
164
|
-
|
|
165
127
|
def _post_file_resource(
|
|
166
128
|
app: ApplicationContext,
|
|
167
129
|
project_id: str,
|
|
@@ -500,10 +462,23 @@ def delete_command(
|
|
|
500
462
|
raise UsageError("Either specify a dataset ID OR" " use a --filter or the --all option.")
|
|
501
463
|
|
|
502
464
|
# Get datasets to delete based on selection method
|
|
503
|
-
|
|
465
|
+
datasets_data = get_objects_to_delete(
|
|
466
|
+
ctx=ctx,
|
|
467
|
+
ids=dataset_ids,
|
|
468
|
+
all_=all_,
|
|
469
|
+
filter_=filter_,
|
|
470
|
+
object_list=dataset_list,
|
|
471
|
+
get_all_objects=lambda: get_datasets_for_list(ctx),
|
|
472
|
+
extract_id=lambda x: x["combinedId"],
|
|
473
|
+
error_message_template=(
|
|
474
|
+
"Dataset {} not available. "
|
|
475
|
+
"Use the 'dataset list' command to get a list of existing datasets."
|
|
476
|
+
),
|
|
477
|
+
id_filter_name="combinedIds",
|
|
478
|
+
)
|
|
504
479
|
|
|
505
480
|
# Avoid double removal as well as sort IDs
|
|
506
|
-
processed_ids = sorted(
|
|
481
|
+
processed_ids = sorted({_["combinedId"] for _ in datasets_data}, key=lambda v: v.lower())
|
|
507
482
|
count = len(processed_ids)
|
|
508
483
|
|
|
509
484
|
# Delete each dataset
|
cmem_cmemc/commands/file.py
CHANGED
|
@@ -27,7 +27,12 @@ from cmem_cmemc.object_list import (
|
|
|
27
27
|
from cmem_cmemc.parameter_types.path import ClickSmartPath
|
|
28
28
|
from cmem_cmemc.smart_path import SmartPath as Path
|
|
29
29
|
from cmem_cmemc.string_processor import FileSize, TimeAgo
|
|
30
|
-
from cmem_cmemc.utils import
|
|
30
|
+
from cmem_cmemc.utils import (
|
|
31
|
+
check_or_select_project,
|
|
32
|
+
get_objects_to_delete,
|
|
33
|
+
split_task_id,
|
|
34
|
+
struct_to_table,
|
|
35
|
+
)
|
|
31
36
|
|
|
32
37
|
|
|
33
38
|
def get_resources(ctx: Context) -> list[dict]: # noqa: ARG001
|
|
@@ -120,48 +125,6 @@ def _upload_file_resource(
|
|
|
120
125
|
app.echo_success("done")
|
|
121
126
|
|
|
122
127
|
|
|
123
|
-
def _validate_resource_ids(resource_ids: tuple[str, ...]) -> None:
|
|
124
|
-
"""Validate that all provided resource IDs exist."""
|
|
125
|
-
if not resource_ids:
|
|
126
|
-
return
|
|
127
|
-
all_resources = get_all_resources()
|
|
128
|
-
all_resource_ids = [_["id"] for _ in all_resources]
|
|
129
|
-
for resource_id in resource_ids:
|
|
130
|
-
if resource_id not in all_resource_ids:
|
|
131
|
-
raise CmemcError(f"Resource {resource_id} not available.")
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def _get_resources_to_delete(
|
|
135
|
-
ctx: Context,
|
|
136
|
-
resource_ids: tuple[str, ...],
|
|
137
|
-
all_: bool,
|
|
138
|
-
filter_: tuple[tuple[str, str], ...],
|
|
139
|
-
) -> list[dict]:
|
|
140
|
-
"""Get the list of resources to delete based on selection method."""
|
|
141
|
-
if all_:
|
|
142
|
-
_: list[dict] = get_all_resources()
|
|
143
|
-
return _
|
|
144
|
-
|
|
145
|
-
# Validate provided IDs exist before proceeding
|
|
146
|
-
_validate_resource_ids(resource_ids)
|
|
147
|
-
|
|
148
|
-
# Build filter list
|
|
149
|
-
filter_to_apply = list(filter_) if filter_ else []
|
|
150
|
-
|
|
151
|
-
# Add IDs if provided (using internal multi-value filter)
|
|
152
|
-
if resource_ids:
|
|
153
|
-
filter_to_apply.append(("ids", ",".join(resource_ids)))
|
|
154
|
-
|
|
155
|
-
# Apply filters
|
|
156
|
-
resources = resource_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
|
|
157
|
-
|
|
158
|
-
# Validation: ensure we found resources
|
|
159
|
-
if not resources and not resource_ids:
|
|
160
|
-
raise CmemcError("No resources found matching the provided filters.")
|
|
161
|
-
|
|
162
|
-
return resources
|
|
163
|
-
|
|
164
|
-
|
|
165
128
|
@click.command(cls=CmemcCommand, name="list")
|
|
166
129
|
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
167
130
|
@click.option(
|
|
@@ -261,8 +224,16 @@ def delete_command(
|
|
|
261
224
|
"--filter options to specify resources for deletion."
|
|
262
225
|
)
|
|
263
226
|
|
|
264
|
-
|
|
265
|
-
|
|
227
|
+
resources_to_delete = get_objects_to_delete(
|
|
228
|
+
ctx=ctx,
|
|
229
|
+
ids=resource_ids,
|
|
230
|
+
all_=all_,
|
|
231
|
+
filter_=filter_,
|
|
232
|
+
object_list=resource_list,
|
|
233
|
+
get_all_objects=get_all_resources,
|
|
234
|
+
extract_id=lambda x: x["id"],
|
|
235
|
+
error_message_template="Resource {} not available.",
|
|
236
|
+
)
|
|
266
237
|
|
|
267
238
|
# Avoid double removal as well as sort IDs
|
|
268
239
|
processed_ids = sorted({_["id"] for _ in resources_to_delete}, key=lambda v: v.lower())
|
cmem_cmemc/commands/graph.py
CHANGED
|
@@ -47,6 +47,7 @@ from cmem_cmemc.utils import (
|
|
|
47
47
|
convert_uri_to_filename,
|
|
48
48
|
get_graphs,
|
|
49
49
|
get_graphs_as_dict,
|
|
50
|
+
get_objects_to_delete,
|
|
50
51
|
iri_to_qname,
|
|
51
52
|
read_rdf_graph_files,
|
|
52
53
|
tuple_to_list,
|
|
@@ -231,49 +232,6 @@ def _add_imported_graphs(iris: list[str], all_graphs: dict) -> list[str]:
|
|
|
231
232
|
return list(set(extended_list))
|
|
232
233
|
|
|
233
234
|
|
|
234
|
-
def _validate_graph_iris(iris: tuple[str, ...]) -> None:
|
|
235
|
-
"""Validate that all provided graph IRIs exist."""
|
|
236
|
-
if not iris:
|
|
237
|
-
return
|
|
238
|
-
all_graphs = get_graphs_as_dict()
|
|
239
|
-
for iri in iris:
|
|
240
|
-
if iri not in all_graphs:
|
|
241
|
-
raise CmemcError(UNKNOWN_GRAPH_ERROR.format(iri))
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def _get_graphs_to_delete(
|
|
245
|
-
ctx: Context,
|
|
246
|
-
iris: tuple[str, ...],
|
|
247
|
-
all_: bool,
|
|
248
|
-
filter_: tuple[tuple[str, str], ...],
|
|
249
|
-
) -> list[dict]:
|
|
250
|
-
"""Get the list of graphs to delete based on selection method."""
|
|
251
|
-
if all_:
|
|
252
|
-
return get_graphs(writeable=True, readonly=False)
|
|
253
|
-
|
|
254
|
-
# Validate provided IRIs exist before proceeding
|
|
255
|
-
_validate_graph_iris(iris)
|
|
256
|
-
|
|
257
|
-
# Build filter list
|
|
258
|
-
filter_to_apply = list(filter_) if filter_ else []
|
|
259
|
-
|
|
260
|
-
# Add IRIs if provided (using internal multi-value filter)
|
|
261
|
-
if iris:
|
|
262
|
-
filter_to_apply.append(("iris", ",".join(iris)))
|
|
263
|
-
|
|
264
|
-
# Apply filters to writeable graphs only
|
|
265
|
-
writeable_graphs = get_graphs(writeable=True, readonly=False)
|
|
266
|
-
graphs = graph_delete_obj.apply_filters(
|
|
267
|
-
ctx=ctx, filter_=filter_to_apply, objects=writeable_graphs
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
# Validation: ensure we found graphs
|
|
271
|
-
if not graphs:
|
|
272
|
-
raise CmemcError("No graphs found matching the provided criteria.")
|
|
273
|
-
|
|
274
|
-
return graphs
|
|
275
|
-
|
|
276
|
-
|
|
277
235
|
def _check_and_extend_exported_graphs(
|
|
278
236
|
iris: list[str], all_flag: bool, imported_flag: bool, all_graphs: dict
|
|
279
237
|
) -> list[str]:
|
|
@@ -919,8 +877,18 @@ def delete_command( # noqa: PLR0913
|
|
|
919
877
|
"--filter options to specify graphs for deletion."
|
|
920
878
|
)
|
|
921
879
|
|
|
922
|
-
# Get base list of graphs to delete
|
|
923
|
-
graphs_to_delete =
|
|
880
|
+
# Get base list of graphs to delete (only writeable graphs)
|
|
881
|
+
graphs_to_delete = get_objects_to_delete(
|
|
882
|
+
ctx=ctx,
|
|
883
|
+
ids=iris,
|
|
884
|
+
all_=all_,
|
|
885
|
+
filter_=filter_,
|
|
886
|
+
object_list=graph_delete_obj,
|
|
887
|
+
get_all_objects=lambda: get_graphs(writeable=True, readonly=False),
|
|
888
|
+
extract_id=lambda x: x["iri"],
|
|
889
|
+
error_message_template=UNKNOWN_GRAPH_ERROR,
|
|
890
|
+
id_filter_name="iris",
|
|
891
|
+
)
|
|
924
892
|
iris_to_delete = [g["iri"] for g in graphs_to_delete]
|
|
925
893
|
|
|
926
894
|
# Handle --include-imports flag
|
|
@@ -27,7 +27,11 @@ from cmem_cmemc.object_list import (
|
|
|
27
27
|
transform_lower,
|
|
28
28
|
)
|
|
29
29
|
from cmem_cmemc.string_processor import GraphLink, TimeAgo
|
|
30
|
-
from cmem_cmemc.utils import
|
|
30
|
+
from cmem_cmemc.utils import (
|
|
31
|
+
get_graphs_as_dict,
|
|
32
|
+
get_objects_to_delete,
|
|
33
|
+
struct_to_table,
|
|
34
|
+
)
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
def get_api_url(path: str = "") -> str:
|
|
@@ -242,6 +246,7 @@ def delete_command(
|
|
|
242
246
|
if snapshot_ids and (all_ or filter_):
|
|
243
247
|
raise click.UsageError("Either specify snapshot IDs OR use a --filter or the --all option.")
|
|
244
248
|
|
|
249
|
+
# Special case: --all uses bulk DELETE endpoint
|
|
245
250
|
if all_:
|
|
246
251
|
app.echo_info("Deleting all snapshots ... ", nl=False)
|
|
247
252
|
request(method="DELETE", uri=get_api_url("/snapshot"))
|
|
@@ -249,20 +254,19 @@ def delete_command(
|
|
|
249
254
|
return
|
|
250
255
|
|
|
251
256
|
# Get snapshots to delete based on selection method
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
snapshots_to_delete = get_objects_to_delete(
|
|
258
|
+
ctx=ctx,
|
|
259
|
+
ids=snapshot_ids,
|
|
260
|
+
all_=False, # Already handled above
|
|
261
|
+
filter_=filter_,
|
|
262
|
+
object_list=snapshot_list,
|
|
263
|
+
get_all_objects=lambda: get_snapshots(ctx),
|
|
264
|
+
extract_id=lambda x: x["databaseId"],
|
|
265
|
+
error_message_template=(
|
|
266
|
+
"Snapshot {} not found. "
|
|
261
267
|
"Use the 'graph insights list' command to get a list of existing snapshots."
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
if not snapshots_to_delete and not snapshot_ids:
|
|
265
|
-
raise CmemcError("No snapshots found to delete.")
|
|
268
|
+
),
|
|
269
|
+
)
|
|
266
270
|
|
|
267
271
|
# Avoid double removal as well as sort IDs
|
|
268
272
|
ids_to_delete = sorted({_["databaseId"] for _ in snapshots_to_delete}, key=lambda v: v.lower())
|
cmem_cmemc/commands/package.py
CHANGED
|
@@ -8,6 +8,7 @@ import click
|
|
|
8
8
|
import requests
|
|
9
9
|
from click.shell_completion import CompletionItem
|
|
10
10
|
from cmem_client.client import Client
|
|
11
|
+
from cmem_client.components.marketplace import Marketplace
|
|
11
12
|
from cmem_client.repositories.marketplace_packages import (
|
|
12
13
|
MarketplacePackagesExportConfig,
|
|
13
14
|
MarketplacePackagesImportConfig,
|
|
@@ -92,6 +93,20 @@ def _complete_installed_package_ids(
|
|
|
92
93
|
return completion.finalize_completion(candidates=candidates, incomplete=incomplete)
|
|
93
94
|
|
|
94
95
|
|
|
96
|
+
@suppress_completion_errors
|
|
97
|
+
def _complete_marketplace_package_ids(
|
|
98
|
+
ctx: click.Context,
|
|
99
|
+
param: click.Argument, # noqa: ARG001
|
|
100
|
+
incomplete: str,
|
|
101
|
+
) -> list[CompletionItem]:
|
|
102
|
+
"""Prepare a list of IDs of available packages."""
|
|
103
|
+
ApplicationContext.set_connection_from_params(ctx.find_root().params)
|
|
104
|
+
client = Client.from_cmempy()
|
|
105
|
+
marketplace = Marketplace(client=client)
|
|
106
|
+
candidates = [(_.id, f"{_.name}") for _ in marketplace.get_available_packages()]
|
|
107
|
+
return completion.finalize_completion(candidates=candidates, incomplete=incomplete)
|
|
108
|
+
|
|
109
|
+
|
|
95
110
|
@click.command(cls=CmemcCommand, name="inspect")
|
|
96
111
|
@click.argument(
|
|
97
112
|
"PACKAGE_PATH",
|
|
@@ -199,6 +214,7 @@ def list_command(
|
|
|
199
214
|
"PACKAGE_ID",
|
|
200
215
|
required=False,
|
|
201
216
|
type=click.STRING,
|
|
217
|
+
shell_complete=_complete_marketplace_package_ids,
|
|
202
218
|
)
|
|
203
219
|
@click.option(
|
|
204
220
|
"--input",
|
|
@@ -418,7 +434,7 @@ def build_command(
|
|
|
418
434
|
|
|
419
435
|
if version_str.endswith("dirty"):
|
|
420
436
|
app.echo_warning(
|
|
421
|
-
"Dirty Repository: Your version string ends with 'dirty'."
|
|
437
|
+
"Dirty Repository: Your version string ends with 'dirty'. "
|
|
422
438
|
"This indicates an unclean repository."
|
|
423
439
|
)
|
|
424
440
|
if version_str == "0.0.0":
|
|
@@ -483,6 +499,58 @@ def publish_command(app: ApplicationContext, package_archive: str, marketplace_u
|
|
|
483
499
|
app.echo_success("done")
|
|
484
500
|
|
|
485
501
|
|
|
502
|
+
@click.command(cls=CmemcCommand, name="search")
|
|
503
|
+
@click.argument(
|
|
504
|
+
"SEARCH_TERMS",
|
|
505
|
+
nargs=-1,
|
|
506
|
+
type=click.STRING,
|
|
507
|
+
)
|
|
508
|
+
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
509
|
+
@click.pass_obj
|
|
510
|
+
def search_command(app: ApplicationContext, search_terms: tuple[str], raw: bool) -> None:
|
|
511
|
+
"""Search for available packages with a given search text."""
|
|
512
|
+
packages = app.client.marketplace.get_available_packages()
|
|
513
|
+
|
|
514
|
+
available_packages = []
|
|
515
|
+
search_terms_lower = [term.lower() for term in search_terms]
|
|
516
|
+
|
|
517
|
+
if search_terms == "":
|
|
518
|
+
available_packages = packages
|
|
519
|
+
else:
|
|
520
|
+
for package in packages:
|
|
521
|
+
searchable_text = (
|
|
522
|
+
f"{package.id.lower()} {package.name.lower()} "
|
|
523
|
+
f"{package.description.lower()} {package.type.value.lower()}"
|
|
524
|
+
)
|
|
525
|
+
if all(term in searchable_text for term in search_terms_lower):
|
|
526
|
+
available_packages.append(package)
|
|
527
|
+
|
|
528
|
+
if raw:
|
|
529
|
+
package_data = [
|
|
530
|
+
json.loads(metadata.model_dump_json(indent=2)) for metadata in available_packages
|
|
531
|
+
]
|
|
532
|
+
app.echo_info_json(package_data)
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
table = [
|
|
536
|
+
(
|
|
537
|
+
_.id,
|
|
538
|
+
_.name,
|
|
539
|
+
_.description,
|
|
540
|
+
_.type.value,
|
|
541
|
+
)
|
|
542
|
+
for _ in available_packages
|
|
543
|
+
]
|
|
544
|
+
app.echo_info_table(
|
|
545
|
+
table,
|
|
546
|
+
headers=["ID", "Name", "Description", "Type"],
|
|
547
|
+
sort_column=0,
|
|
548
|
+
empty_table_message="No available packages found.",
|
|
549
|
+
caption=f"{len(available_packages)} package{'' if len(available_packages) == 1 else "s" } "
|
|
550
|
+
f"found on marketplace.eccenca.dev",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
486
554
|
@click.group(cls=CmemcGroup)
|
|
487
555
|
def package_group() -> CmemcGroup: # type: ignore[empty-body]
|
|
488
556
|
"""List, (un)install, export, create, or inspect packages."""
|
|
@@ -495,3 +563,4 @@ package_group.add_command(uninstall_command)
|
|
|
495
563
|
package_group.add_command(export_command)
|
|
496
564
|
package_group.add_command(build_command)
|
|
497
565
|
package_group.add_command(publish_command)
|
|
566
|
+
package_group.add_command(search_command)
|
cmem_cmemc/commands/project.py
CHANGED
|
@@ -38,7 +38,7 @@ from cmem_cmemc.context import ApplicationContext, build_caption
|
|
|
38
38
|
from cmem_cmemc.exceptions import CmemcError
|
|
39
39
|
from cmem_cmemc.object_list import (
|
|
40
40
|
DirectListPropertyFilter,
|
|
41
|
-
|
|
41
|
+
DirectMultiValuePropertyFilter,
|
|
42
42
|
Filter,
|
|
43
43
|
MultiFieldPropertyFilter,
|
|
44
44
|
ObjectList,
|
|
@@ -46,6 +46,7 @@ from cmem_cmemc.object_list import (
|
|
|
46
46
|
from cmem_cmemc.parameter_types.path import ClickSmartPath
|
|
47
47
|
from cmem_cmemc.smart_path import SmartPath as Path
|
|
48
48
|
from cmem_cmemc.string_processor import ProjectLink, TimeAgo
|
|
49
|
+
from cmem_cmemc.utils import get_objects_to_delete
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def get_projects_for_list(ctx: Context) -> list[dict]:
|
|
@@ -90,11 +91,10 @@ project_list = ObjectList(
|
|
|
90
91
|
name="projects",
|
|
91
92
|
get_objects=get_projects_for_list,
|
|
92
93
|
filters=[
|
|
93
|
-
|
|
94
|
-
name="
|
|
95
|
-
description="
|
|
94
|
+
DirectMultiValuePropertyFilter(
|
|
95
|
+
name="ids",
|
|
96
|
+
description="Internal filter for multiple projects.",
|
|
96
97
|
property_key="name",
|
|
97
|
-
completion_method="values",
|
|
98
98
|
),
|
|
99
99
|
MultiFieldPropertyFilter(
|
|
100
100
|
name="regex",
|
|
@@ -296,15 +296,20 @@ def delete_command(
|
|
|
296
296
|
if project_ids and (all_ or filter_):
|
|
297
297
|
raise UsageError("Either specify a project ID OR use a --filter or the --all option.")
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
299
|
+
# Get projects to delete based on selection method
|
|
300
|
+
projects_data = get_objects_to_delete(
|
|
301
|
+
ctx=ctx,
|
|
302
|
+
ids=project_ids,
|
|
303
|
+
all_=all_,
|
|
304
|
+
filter_=filter_,
|
|
305
|
+
object_list=project_list,
|
|
306
|
+
get_all_objects=get_projects,
|
|
307
|
+
extract_id=lambda x: x["name"],
|
|
308
|
+
error_message_template="Project {} not available.",
|
|
309
|
+
)
|
|
305
310
|
|
|
306
311
|
# Avoid double removal as well as sort project IDs
|
|
307
|
-
projects_to_delete = sorted(
|
|
312
|
+
projects_to_delete = sorted({_["name"] for _ in projects_data}, key=lambda v: v.lower())
|
|
308
313
|
count = len(projects_to_delete)
|
|
309
314
|
for current, project_id in enumerate(projects_to_delete, start=1):
|
|
310
315
|
current_string = str(current).zfill(len(str(count)))
|
cmem_cmemc/commands/user.py
CHANGED
|
@@ -31,6 +31,7 @@ from cmem_cmemc.object_list import (
|
|
|
31
31
|
compare_regex,
|
|
32
32
|
transform_lower,
|
|
33
33
|
)
|
|
34
|
+
from cmem_cmemc.utils import get_objects_to_delete
|
|
34
35
|
|
|
35
36
|
NO_USER_ERROR = (
|
|
36
37
|
"{} is not a valid user account. Use the 'admin user list' command "
|
|
@@ -81,52 +82,6 @@ user_list = ObjectList(
|
|
|
81
82
|
)
|
|
82
83
|
|
|
83
84
|
|
|
84
|
-
def _validate_usernames(usernames: tuple[str, ...]) -> None:
|
|
85
|
-
"""Validate that all provided usernames exist."""
|
|
86
|
-
if not usernames:
|
|
87
|
-
return
|
|
88
|
-
all_users = list_users()
|
|
89
|
-
all_usernames = [user["username"] for user in all_users]
|
|
90
|
-
for username in usernames:
|
|
91
|
-
if username not in all_usernames:
|
|
92
|
-
raise CmemcError(NO_USER_ERROR.format(username))
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _get_users_to_delete(
|
|
96
|
-
ctx: click.Context,
|
|
97
|
-
usernames: tuple[str, ...],
|
|
98
|
-
all_: bool,
|
|
99
|
-
filter_: tuple[tuple[str, str], ...],
|
|
100
|
-
) -> list[str]:
|
|
101
|
-
"""Get the list of usernames to delete based on selection method."""
|
|
102
|
-
if all_:
|
|
103
|
-
# Get all users
|
|
104
|
-
users = list_users()
|
|
105
|
-
return [user["username"] for user in users]
|
|
106
|
-
|
|
107
|
-
# Validate provided usernames exist before proceeding
|
|
108
|
-
_validate_usernames(usernames)
|
|
109
|
-
|
|
110
|
-
# Build filter list
|
|
111
|
-
filter_to_apply = list(filter_) if filter_ else []
|
|
112
|
-
|
|
113
|
-
# Add usernames if provided (using internal multi-value filter)
|
|
114
|
-
if usernames:
|
|
115
|
-
filter_to_apply.append(("usernames", ",".join(usernames)))
|
|
116
|
-
|
|
117
|
-
# Apply filters
|
|
118
|
-
users = user_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
|
|
119
|
-
|
|
120
|
-
# Build list of usernames
|
|
121
|
-
result = [user["username"] for user in users]
|
|
122
|
-
|
|
123
|
-
# Validation: ensure we found users
|
|
124
|
-
if not result and not usernames:
|
|
125
|
-
raise CmemcError("No user accounts found matching the provided filters.")
|
|
126
|
-
|
|
127
|
-
return result
|
|
128
|
-
|
|
129
|
-
|
|
130
85
|
@click.command(cls=CmemcCommand, name="list")
|
|
131
86
|
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
132
87
|
@click.option(
|
|
@@ -228,11 +183,20 @@ def delete_command(
|
|
|
228
183
|
if usernames and (all_ or filter_):
|
|
229
184
|
raise click.UsageError("Either specify a username OR use a --filter or the --all option.")
|
|
230
185
|
|
|
231
|
-
|
|
232
|
-
|
|
186
|
+
users_data = get_objects_to_delete(
|
|
187
|
+
ctx=ctx,
|
|
188
|
+
ids=usernames,
|
|
189
|
+
all_=all_,
|
|
190
|
+
filter_=filter_,
|
|
191
|
+
object_list=user_list,
|
|
192
|
+
get_all_objects=list_users,
|
|
193
|
+
extract_id=lambda x: x["username"],
|
|
194
|
+
error_message_template=NO_USER_ERROR,
|
|
195
|
+
id_filter_name="usernames",
|
|
196
|
+
)
|
|
233
197
|
|
|
234
198
|
# Avoid double removal as well as sort usernames
|
|
235
|
-
processed_usernames = sorted(
|
|
199
|
+
processed_usernames = sorted({user["username"] for user in users_data}, key=lambda v: v.lower())
|
|
236
200
|
count = len(processed_usernames)
|
|
237
201
|
|
|
238
202
|
# Delete each user
|
cmem_cmemc/commands/variable.py
CHANGED
|
@@ -17,7 +17,6 @@ from cmem_cmemc import completion
|
|
|
17
17
|
from cmem_cmemc.command import CmemcCommand
|
|
18
18
|
from cmem_cmemc.command_group import CmemcGroup
|
|
19
19
|
from cmem_cmemc.context import ApplicationContext, build_caption
|
|
20
|
-
from cmem_cmemc.exceptions import CmemcError
|
|
21
20
|
from cmem_cmemc.object_list import (
|
|
22
21
|
DirectMultiValuePropertyFilter,
|
|
23
22
|
DirectValuePropertyFilter,
|
|
@@ -25,7 +24,11 @@ from cmem_cmemc.object_list import (
|
|
|
25
24
|
ObjectList,
|
|
26
25
|
compare_regex,
|
|
27
26
|
)
|
|
28
|
-
from cmem_cmemc.utils import
|
|
27
|
+
from cmem_cmemc.utils import (
|
|
28
|
+
check_or_select_project,
|
|
29
|
+
get_objects_to_delete,
|
|
30
|
+
split_task_id,
|
|
31
|
+
)
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
def get_variables(ctx: Context) -> list[dict]: # noqa: ARG001
|
|
@@ -172,41 +175,6 @@ def _sort_variables_by_dependency(variables: list[dict]) -> list[str]:
|
|
|
172
175
|
return result
|
|
173
176
|
|
|
174
177
|
|
|
175
|
-
def _validate_variable_ids(variable_ids: tuple[str, ...]) -> None:
|
|
176
|
-
"""Validate that provided variable IDs exist."""
|
|
177
|
-
all_variables = get_all_variables()
|
|
178
|
-
all_variable_ids = [_["id"] for _ in all_variables]
|
|
179
|
-
for variable_id in variable_ids:
|
|
180
|
-
if variable_id not in all_variable_ids:
|
|
181
|
-
raise CmemcError(f"Variable {variable_id} not available.")
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def _get_variables_to_delete(
|
|
185
|
-
ctx: Context,
|
|
186
|
-
variable_ids: tuple[str, ...],
|
|
187
|
-
all_: bool,
|
|
188
|
-
filter_: tuple[tuple[str, str], ...],
|
|
189
|
-
) -> list[dict]:
|
|
190
|
-
"""Get the list of variables to delete based on selection method."""
|
|
191
|
-
if all_:
|
|
192
|
-
_: list[dict] = get_all_variables()
|
|
193
|
-
return _
|
|
194
|
-
|
|
195
|
-
_validate_variable_ids(variable_ids)
|
|
196
|
-
|
|
197
|
-
filter_to_apply = list(filter_) if filter_ else []
|
|
198
|
-
|
|
199
|
-
if variable_ids:
|
|
200
|
-
filter_to_apply.append(("ids", ",".join(variable_ids)))
|
|
201
|
-
|
|
202
|
-
variables = variable_list_obj.apply_filters(ctx=ctx, filter_=filter_to_apply)
|
|
203
|
-
|
|
204
|
-
if not variables and not variable_ids:
|
|
205
|
-
raise CmemcError("No variables found matching the provided filters.")
|
|
206
|
-
|
|
207
|
-
return variables
|
|
208
|
-
|
|
209
|
-
|
|
210
178
|
@click.command(cls=CmemcCommand, name="list")
|
|
211
179
|
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
212
180
|
@click.option(
|
|
@@ -340,8 +308,16 @@ def delete_command(
|
|
|
340
308
|
"--filter options to specify variables for deletion."
|
|
341
309
|
)
|
|
342
310
|
|
|
343
|
-
|
|
344
|
-
|
|
311
|
+
variables_to_delete = get_objects_to_delete(
|
|
312
|
+
ctx=ctx,
|
|
313
|
+
ids=variable_ids,
|
|
314
|
+
all_=all_,
|
|
315
|
+
filter_=filter_,
|
|
316
|
+
object_list=variable_list_obj,
|
|
317
|
+
get_all_objects=get_all_variables,
|
|
318
|
+
extract_id=lambda x: x["id"],
|
|
319
|
+
error_message_template="Variable {} not available.",
|
|
320
|
+
)
|
|
345
321
|
|
|
346
322
|
# Remove duplicates while preserving variable objects for dependency analysis
|
|
347
323
|
unique_variables = list({v["id"]: v for v in variables_to_delete}.values())
|
cmem_cmemc/context.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import ast
|
|
4
4
|
import configparser
|
|
5
5
|
import json
|
|
6
|
+
import logging
|
|
6
7
|
import os
|
|
7
8
|
import re
|
|
8
9
|
import subprocess # nosec
|
|
@@ -57,6 +58,19 @@ KNOWN_SECRET_KEYS = ("OAUTH_PASSWORD", "OAUTH_CLIENT_SECRET", "OAUTH_ACCESS_TOKE
|
|
|
57
58
|
SSL_VERIFY_WARNING = "SSL verification is disabled (SSL_VERIFY=False)."
|
|
58
59
|
|
|
59
60
|
|
|
61
|
+
class ClickHandler(logging.Handler):
|
|
62
|
+
"""Custom logging handler that outputs using click.secho with dimming."""
|
|
63
|
+
|
|
64
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
65
|
+
"""Emit a log record using click.secho."""
|
|
66
|
+
try:
|
|
67
|
+
now = datetime.now(tz=timezone.utc)
|
|
68
|
+
msg = self.format(record)
|
|
69
|
+
click.secho(f"[{now!s}] {msg}", err=True, dim=True)
|
|
70
|
+
except Exception: # noqa: BLE001
|
|
71
|
+
self.handleError(record)
|
|
72
|
+
|
|
73
|
+
|
|
60
74
|
def build_caption(
|
|
61
75
|
count: int,
|
|
62
76
|
item_name: str,
|
|
@@ -101,12 +115,14 @@ class ApplicationContext:
|
|
|
101
115
|
connection: str | None
|
|
102
116
|
console: Console
|
|
103
117
|
console_width: int | None = None
|
|
118
|
+
log_level: str | None = None
|
|
104
119
|
|
|
105
120
|
def __init__(
|
|
106
121
|
self,
|
|
107
122
|
config_file: str,
|
|
108
123
|
connection: str | None = None,
|
|
109
124
|
debug: bool = False,
|
|
125
|
+
log_level: str | None = None,
|
|
110
126
|
quiet: bool = False,
|
|
111
127
|
):
|
|
112
128
|
"""Initialize main context."""
|
|
@@ -114,6 +130,7 @@ class ApplicationContext:
|
|
|
114
130
|
self.app_name = "cmemc"
|
|
115
131
|
self.set_debug(debug)
|
|
116
132
|
self.set_quiet(quiet)
|
|
133
|
+
self.log_level = log_level
|
|
117
134
|
self.ensure_app_config_dir()
|
|
118
135
|
self.set_connection(connection)
|
|
119
136
|
self.set_external_http_timeout(self.DEFAULT_EXTERNAL_HTTP_TIMEOUT)
|
|
@@ -123,7 +140,18 @@ class ApplicationContext:
|
|
|
123
140
|
@property
|
|
124
141
|
def client(self) -> Client:
|
|
125
142
|
"""The cmem_client Client object."""
|
|
126
|
-
|
|
143
|
+
client = Client.from_cmempy()
|
|
144
|
+
if self.debug:
|
|
145
|
+
level = self.log_level or "DEBUG"
|
|
146
|
+
handler = ClickHandler()
|
|
147
|
+
formatter = logging.Formatter("%(name)s - [%(levelname)s] - %(message)s")
|
|
148
|
+
handler.setFormatter(formatter)
|
|
149
|
+
client.configure_client_logger(
|
|
150
|
+
level=level,
|
|
151
|
+
format_string="%(name)s - [%(levelname)s] - %(message)s",
|
|
152
|
+
handlers=[handler],
|
|
153
|
+
)
|
|
154
|
+
return client
|
|
127
155
|
|
|
128
156
|
@staticmethod
|
|
129
157
|
def from_params(params: dict) -> "ApplicationContext":
|
|
@@ -134,6 +162,7 @@ class ApplicationContext:
|
|
|
134
162
|
config_file = params.get("config_file")
|
|
135
163
|
debug = str_to_bool(str(params.get("debug")))
|
|
136
164
|
quiet = str_to_bool(str(params.get("quiet")))
|
|
165
|
+
log_level = str(params.get("log_level")) if params.get("log_level") else None
|
|
137
166
|
external_http_timeout = (
|
|
138
167
|
int(str(params.get("external_http_timeout")))
|
|
139
168
|
if "external_http_timeout" in params
|
|
@@ -143,7 +172,11 @@ class ApplicationContext:
|
|
|
143
172
|
if not config_file:
|
|
144
173
|
raise ValueError("Missing required key: 'config_file' in config dictionary")
|
|
145
174
|
app = ApplicationContext(
|
|
146
|
-
config_file=config_file,
|
|
175
|
+
config_file=config_file,
|
|
176
|
+
connection=connection,
|
|
177
|
+
debug=debug,
|
|
178
|
+
quiet=quiet,
|
|
179
|
+
log_level=log_level,
|
|
147
180
|
)
|
|
148
181
|
app.set_external_http_timeout(external_http_timeout)
|
|
149
182
|
if not app.debug:
|
|
@@ -184,6 +217,10 @@ class ApplicationContext:
|
|
|
184
217
|
"""Set debug state"""
|
|
185
218
|
self.debug = debug
|
|
186
219
|
|
|
220
|
+
def set_log_level(self, log_level: str) -> None:
|
|
221
|
+
"""Set log level"""
|
|
222
|
+
self.log_level = log_level
|
|
223
|
+
|
|
187
224
|
def set_quiet(self, quiet: bool = False) -> None:
|
|
188
225
|
"""Set quiets state"""
|
|
189
226
|
self.quiet = quiet
|
|
@@ -247,6 +284,9 @@ class ApplicationContext:
|
|
|
247
284
|
for key, value in config.items():
|
|
248
285
|
if key == "CMEMC_DEBUG":
|
|
249
286
|
self.set_debug(str_to_bool(value))
|
|
287
|
+
elif key == "CMEMC_LOG_LEVEL":
|
|
288
|
+
self.set_log_level(value)
|
|
289
|
+
self.echo_debug(f"set log level to {value}")
|
|
250
290
|
elif key == "CMEMC_QUIET":
|
|
251
291
|
self.set_quiet(str_to_bool(value))
|
|
252
292
|
elif key == "CMEMC_CONNECTION":
|
cmem_cmemc/utils.py
CHANGED
|
@@ -6,11 +6,13 @@ import pathlib
|
|
|
6
6
|
import re
|
|
7
7
|
import sys
|
|
8
8
|
import unicodedata
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from dataclasses import dataclass
|
|
10
11
|
from importlib.metadata import version as cmemc_version
|
|
11
12
|
from typing import TYPE_CHECKING
|
|
12
13
|
from zipfile import BadZipFile, ZipFile
|
|
13
14
|
|
|
15
|
+
import click
|
|
14
16
|
import requests
|
|
15
17
|
from click import Argument
|
|
16
18
|
from cmem.cmempy.dp.proxy.graph import get_graphs_list
|
|
@@ -24,6 +26,7 @@ from cmem_cmemc.exceptions import CmemcError
|
|
|
24
26
|
from cmem_cmemc.smart_path import SmartPath
|
|
25
27
|
|
|
26
28
|
if TYPE_CHECKING:
|
|
29
|
+
from cmem_cmemc import object_list
|
|
27
30
|
from cmem_cmemc.context import ApplicationContext
|
|
28
31
|
|
|
29
32
|
|
|
@@ -453,3 +456,113 @@ def tuple_to_list(ctx: type["ApplicationContext"], param: Argument, value: tuple
|
|
|
453
456
|
Used as callback to have mutable values
|
|
454
457
|
"""
|
|
455
458
|
return list(value)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def validate_ids(
|
|
462
|
+
ids: tuple[str, ...],
|
|
463
|
+
get_all_objects: Callable[[], list[dict]],
|
|
464
|
+
extract_id: Callable[[dict], str],
|
|
465
|
+
error_message_template: str,
|
|
466
|
+
) -> None:
|
|
467
|
+
"""Validate that all provided IDs exist.
|
|
468
|
+
|
|
469
|
+
This is a reusable validation function for delete commands.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
----
|
|
473
|
+
ids: Tuple of identifiers to validate
|
|
474
|
+
get_all_objects: Callable that returns list of all objects (e.g., list_items, get_graphs)
|
|
475
|
+
extract_id: Callable that extracts the ID from an object dict (e.g., lambda x: x["iri"])
|
|
476
|
+
error_message_template: Error message template with {} placeholder for the missing ID
|
|
477
|
+
|
|
478
|
+
Raises:
|
|
479
|
+
------
|
|
480
|
+
CmemcError: If any ID is not found
|
|
481
|
+
|
|
482
|
+
Example:
|
|
483
|
+
-------
|
|
484
|
+
validate_ids(
|
|
485
|
+
ids=("graph1", "graph2"),
|
|
486
|
+
get_all_objects=lambda: get_graphs(),
|
|
487
|
+
extract_id=lambda x: x["iri"],
|
|
488
|
+
error_message_template="{} is not a valid graph IRI."
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
"""
|
|
492
|
+
if not ids:
|
|
493
|
+
return
|
|
494
|
+
all_objects = get_all_objects()
|
|
495
|
+
all_ids = {extract_id(obj) for obj in all_objects}
|
|
496
|
+
for id_ in ids:
|
|
497
|
+
if id_ not in all_ids:
|
|
498
|
+
raise CmemcError(error_message_template.format(id_))
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def get_objects_to_delete( # noqa: PLR0913
|
|
502
|
+
ctx: click.Context,
|
|
503
|
+
ids: tuple[str, ...],
|
|
504
|
+
all_: bool,
|
|
505
|
+
filter_: tuple[tuple[str, str], ...],
|
|
506
|
+
object_list: "object_list.ObjectList",
|
|
507
|
+
get_all_objects: Callable[[], list[dict]],
|
|
508
|
+
extract_id: Callable[[dict], str],
|
|
509
|
+
error_message_template: str,
|
|
510
|
+
id_filter_name: str = "ids",
|
|
511
|
+
) -> list[dict]:
|
|
512
|
+
"""Get the list of objects to delete based on selection method.
|
|
513
|
+
|
|
514
|
+
This is a reusable utility for delete commands that encapsulates the common pattern of:
|
|
515
|
+
1. If --all flag is set, return all objects
|
|
516
|
+
2. Validate provided IDs exist
|
|
517
|
+
3. Build filter list with IDs (using internal multi-value filter)
|
|
518
|
+
4. Apply filters using ObjectList
|
|
519
|
+
5. Validate that objects were found
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
----
|
|
523
|
+
ctx: Click context
|
|
524
|
+
ids: Tuple of specific identifiers to delete
|
|
525
|
+
all_: Flag to delete all objects
|
|
526
|
+
filter_: Tuple of filter name/value pairs
|
|
527
|
+
object_list: ObjectList instance for filtering
|
|
528
|
+
get_all_objects: Callable that returns list of all objects (for validation)
|
|
529
|
+
extract_id: Callable that extracts the ID from an object dict
|
|
530
|
+
error_message_template: Template string for error messages
|
|
531
|
+
id_filter_name: Name of the internal multi-value filter (default: "ids")
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
-------
|
|
535
|
+
List of object dictionaries to delete
|
|
536
|
+
|
|
537
|
+
Raises:
|
|
538
|
+
------
|
|
539
|
+
CmemcError: If no objects found matching the criteria
|
|
540
|
+
|
|
541
|
+
"""
|
|
542
|
+
if all_:
|
|
543
|
+
return object_list.apply_filters(ctx=ctx, filter_=[])
|
|
544
|
+
|
|
545
|
+
# Validate provided IDs exist before proceeding
|
|
546
|
+
if ids:
|
|
547
|
+
validate_ids(
|
|
548
|
+
ids=ids,
|
|
549
|
+
get_all_objects=get_all_objects,
|
|
550
|
+
extract_id=extract_id,
|
|
551
|
+
error_message_template=error_message_template,
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
# Build filter list
|
|
555
|
+
filter_to_apply = list(filter_) if filter_ else []
|
|
556
|
+
|
|
557
|
+
# Add IDs if provided (using internal multi-value filter)
|
|
558
|
+
if ids:
|
|
559
|
+
filter_to_apply.append((id_filter_name, ",".join(ids)))
|
|
560
|
+
|
|
561
|
+
# Apply filters
|
|
562
|
+
objects = object_list.apply_filters(ctx=ctx, filter_=filter_to_apply)
|
|
563
|
+
|
|
564
|
+
# Validation: ensure we found objects
|
|
565
|
+
if not objects:
|
|
566
|
+
raise CmemcError("No objects found matching the provided criteria.")
|
|
567
|
+
|
|
568
|
+
return objects
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cmem-cmemc
|
|
3
|
-
Version: 26.1.
|
|
3
|
+
Version: 26.1.0rc4
|
|
4
4
|
Summary: Command line client for eccenca Corporate Memory
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -32,7 +32,7 @@ Requires-Dist: certifi (>=2024.2.2)
|
|
|
32
32
|
Requires-Dist: click (>=8.3.0,<9.0.0)
|
|
33
33
|
Requires-Dist: click-didyoumean (>=0.3.1,<0.4.0)
|
|
34
34
|
Requires-Dist: click-help-colors (>=0.9.4,<0.10.0)
|
|
35
|
-
Requires-Dist: cmem-client (==0.
|
|
35
|
+
Requires-Dist: cmem-client (==0.8.0)
|
|
36
36
|
Requires-Dist: cmem-cmempy (==25.4.0)
|
|
37
37
|
Requires-Dist: configparser (>=7.2.0,<8.0.0)
|
|
38
38
|
Requires-Dist: humanize (>=4.14.0,<5.0.0)
|
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
cmem_cmemc/__init__.py,sha256=-RPEVweA-fcmEAynszDDMKwArJgxZpGW61UBiV7O4Og,24
|
|
2
2
|
cmem_cmemc/_cmemc.zsh,sha256=fmkrBHIQxus8cp2AgO1tzZ5mNZdGL_83cYz3a9uAdsg,1326
|
|
3
|
-
cmem_cmemc/cli.py,sha256=
|
|
3
|
+
cmem_cmemc/cli.py,sha256=V0BUSN-N7HzGtNBZOZ8N6C_8FaofCTD13uuuuW9f79k,5368
|
|
4
4
|
cmem_cmemc/command.py,sha256=CuaskaqD12soZLhDP1prgXOT4cRFu1CzuJm6LBp4zLM,1949
|
|
5
5
|
cmem_cmemc/command_group.py,sha256=0I2Jg1lCdoozcMg7d0g9sk0zVJ8_LiKvWWwqM6tZGI0,4793
|
|
6
6
|
cmem_cmemc/commands/__init__.py,sha256=NaGM5jOzf0S_-4UIAwlVDOf2AZ3mliGPoRLXQJfTyZs,22
|
|
7
|
-
cmem_cmemc/commands/acl.py,sha256=
|
|
7
|
+
cmem_cmemc/commands/acl.py,sha256=X7RMwEO66SvOY4gXrlBJvKTUYiUdURf0YLaZx1Feqx8,30238
|
|
8
8
|
cmem_cmemc/commands/admin.py,sha256=XyJVk9CuxLh60GdWNtMkGPpZ3BOftDF4T04M8gRaOQ0,10193
|
|
9
9
|
cmem_cmemc/commands/client.py,sha256=4Ke9Pl6-KQ-6_uSpRbVhB7spAi8tP0r39y99fwFRSWE,5304
|
|
10
10
|
cmem_cmemc/commands/config.py,sha256=tRB7YK6vzkaasmyZVWGLOw1_x0SM2x-coYMBRIJwEJs,8392
|
|
11
|
-
cmem_cmemc/commands/dataset.py,sha256=
|
|
12
|
-
cmem_cmemc/commands/file.py,sha256=
|
|
13
|
-
cmem_cmemc/commands/graph.py,sha256=
|
|
11
|
+
cmem_cmemc/commands/dataset.py,sha256=ZdiKLXWq9ItnJb4xGiP0qaby5W3Kvkyqg8KIaYsHelI,29715
|
|
12
|
+
cmem_cmemc/commands/file.py,sha256=Ds7vzaiWIfnOesxS9KQQuBinlkSjcSqV_02SX1ad_sE,15926
|
|
13
|
+
cmem_cmemc/commands/graph.py,sha256=oUInQQEuL-n6bFEscNUB7qfPUk0yNajEy0H5VFXFPoQ,35667
|
|
14
14
|
cmem_cmemc/commands/graph_imports.py,sha256=VcOisHSvNYJPxMiRb6sHEswYIQHwlIamLwQD1jgr_e0,14368
|
|
15
|
-
cmem_cmemc/commands/graph_insights.py,sha256=
|
|
15
|
+
cmem_cmemc/commands/graph_insights.py,sha256=slQXBUy7azEG-w7M-2r9ybY4J3HgV9TTWbP5Ih3HDu0,14395
|
|
16
16
|
cmem_cmemc/commands/manual.py,sha256=-sZWeFL92Kj8gL3VYsbpKh2ZaVTyM3LgKaUcpNn9u3A,2179
|
|
17
17
|
cmem_cmemc/commands/metrics.py,sha256=t5I6VjBzjp_bQEoGkU9cdqNu_sa_WQwiIeJA3f9KNWc,12385
|
|
18
18
|
cmem_cmemc/commands/migration.py,sha256=FibmYpvZD2mrutjyRBhs7xDAZ-sjPiH9Kn3CI-zUPm0,9861
|
|
19
|
-
cmem_cmemc/commands/package.py,sha256=
|
|
20
|
-
cmem_cmemc/commands/project.py,sha256=
|
|
19
|
+
cmem_cmemc/commands/package.py,sha256=m5IBnSZdwroewHBTyxDIjQrF0NSixmMdffL1OeszyQM,18480
|
|
20
|
+
cmem_cmemc/commands/project.py,sha256=HRA4xpOENajyQub1iWLo5voAsXFUzFPt7kJq9sx4sx4,25066
|
|
21
21
|
cmem_cmemc/commands/python.py,sha256=lcbBAYZN5NB37HLSmVPs0SXJV7Ey4xVMYQiSiuyGkvc,12225
|
|
22
22
|
cmem_cmemc/commands/query.py,sha256=1cj1QbvwL98YbBGSCO0Zazbzscts_kiv0A7k75KwJXw,32231
|
|
23
23
|
cmem_cmemc/commands/scheduler.py,sha256=3wk3BF6Z3uRb0e5pphOYBusbXgs7C6Lz-D9wi7Nlohc,8855
|
|
24
24
|
cmem_cmemc/commands/store.py,sha256=zKz8FTtVSvFU6gMm6An7Jja9Bu9dZKbI1GW7UCq034s,10655
|
|
25
|
-
cmem_cmemc/commands/user.py,sha256=
|
|
25
|
+
cmem_cmemc/commands/user.py,sha256=Uqhqn5gb4Fy4_ZaJbAwVFTRERvRO7A0yYO2ic6Xz1Wc,14707
|
|
26
26
|
cmem_cmemc/commands/validation.py,sha256=v9_cXGzaexemuz6xBA358XY1_vP42SBfOD3PEZLcqbw,29731
|
|
27
|
-
cmem_cmemc/commands/variable.py,sha256=
|
|
27
|
+
cmem_cmemc/commands/variable.py,sha256=LRgZ82lubDnY0yh67KBF0se2W1PeE58pYAJuwftbfyE,17926
|
|
28
28
|
cmem_cmemc/commands/vocabulary.py,sha256=sv7hDZOeRPrPlc5RJfpAKzKH5JRyKjB93D-Jl4eLNqI,18359
|
|
29
29
|
cmem_cmemc/commands/workflow.py,sha256=UdKAsY3chxfrIpkjL1K9MqLVyu7jTdFYkOXfGVvJISI,26369
|
|
30
30
|
cmem_cmemc/commands/workspace.py,sha256=IcZgBsvtulLRFofS70qpln6oKQIZunrVLfSAUeiFhCA,4579
|
|
31
31
|
cmem_cmemc/completion.py,sha256=JbMZmTLjgu_nrIS9NuuFHqfqAFwHE1dCvFNk0g2c6d0,44805
|
|
32
32
|
cmem_cmemc/config_parser.py,sha256=NduwOT-BB_uAk3pz1Y-ex18RQJW-jjHzkQKCEUUK6Hc,1276
|
|
33
33
|
cmem_cmemc/constants.py,sha256=pzZYbSaTDUiWmE-VOAHB20oivHew5_FP9UTejySsVK4,550
|
|
34
|
-
cmem_cmemc/context.py,sha256=
|
|
34
|
+
cmem_cmemc/context.py,sha256=miYUlpJ9QwEPoh8U8_sekBQXw3MVe9kR5wGOWCzb0Yo,24683
|
|
35
35
|
cmem_cmemc/exceptions.py,sha256=c4Z6CKgymu0a7gD8MtHxzK_7WCsb9I2Zl-EgEkwu-YY,760
|
|
36
36
|
cmem_cmemc/manual_helper/__init__.py,sha256=G3Lqw2aPxo8x63Tg7L0aa5VD9BMaRzZDmhrog7IuEPg,43
|
|
37
37
|
cmem_cmemc/manual_helper/graph.py,sha256=dTkFXgU9fgySn54rE93t79v1MjWjQkprKRIfJhc7Jps,3655
|
|
@@ -54,9 +54,9 @@ cmem_cmemc/smart_path/clients/__init__.py,sha256=YFOm69BfTCRvAcJjN_CoUmCv3kzEciy
|
|
|
54
54
|
cmem_cmemc/smart_path/clients/http.py,sha256=3clZu2v4uuOvPY4MY_8SVSy7hIXJDNooahFRBRpy0ok,2347
|
|
55
55
|
cmem_cmemc/string_processor.py,sha256=19YSLUF9PIbfTmsTm2bZslsNhFUAYx0MerWYwC3BVEo,8616
|
|
56
56
|
cmem_cmemc/title_helper.py,sha256=8Cyes2U4lHTQbzYwBSYqCrZbq29_oBg6uibe7xZ6DEg,3486
|
|
57
|
-
cmem_cmemc/utils.py,sha256=
|
|
58
|
-
cmem_cmemc-26.1.
|
|
59
|
-
cmem_cmemc-26.1.
|
|
60
|
-
cmem_cmemc-26.1.
|
|
61
|
-
cmem_cmemc-26.1.
|
|
62
|
-
cmem_cmemc-26.1.
|
|
57
|
+
cmem_cmemc/utils.py,sha256=rs3qf5UZeiTQO0USUpFQq6upQnG_S43CW7YYUrCwmzk,18240
|
|
58
|
+
cmem_cmemc-26.1.0rc4.dist-info/METADATA,sha256=ViY7R6xoIspCb6bu7it8j94FJDu7ks-6t_1iotPLDw8,5754
|
|
59
|
+
cmem_cmemc-26.1.0rc4.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
60
|
+
cmem_cmemc-26.1.0rc4.dist-info/entry_points.txt,sha256=2G0AWAyz501EHpFTjIxccdlCTsHt80NT0pdUGP1QkPA,45
|
|
61
|
+
cmem_cmemc-26.1.0rc4.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
62
|
+
cmem_cmemc-26.1.0rc4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|