cmem-cmemc 25.6.0__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 +27 -0
- cmem_cmemc/commands/acl.py +388 -20
- 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 +162 -118
- cmem_cmemc/commands/file.py +117 -73
- cmem_cmemc/commands/graph.py +200 -72
- cmem_cmemc/commands/graph_imports.py +12 -5
- cmem_cmemc/commands/graph_insights.py +61 -25
- 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 +155 -22
- cmem_cmemc/commands/python.py +8 -4
- 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 +18 -13
- cmem_cmemc/commands/workflow.py +21 -11
- cmem_cmemc/completion.py +105 -105
- cmem_cmemc/context.py +38 -8
- cmem_cmemc/exceptions.py +8 -2
- cmem_cmemc/manual_helper/multi_page.py +0 -1
- cmem_cmemc/object_list.py +234 -7
- cmem_cmemc/string_processor.py +142 -5
- cmem_cmemc/title_helper.py +50 -0
- cmem_cmemc/utils.py +8 -7
- {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/METADATA +6 -6
- cmem_cmemc-26.1.0rc1.dist-info/RECORD +62 -0
- {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/WHEEL +1 -1
- cmem_cmemc-25.6.0.dist-info/RECORD +0 -61
- {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/entry_points.txt +0 -0
- {cmem_cmemc-25.6.0.dist-info → cmem_cmemc-26.1.0rc1.dist-info}/licenses/LICENSE +0 -0
cmem_cmemc/commands/user.py
CHANGED
|
@@ -4,7 +4,6 @@ import sys
|
|
|
4
4
|
from getpass import getpass
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
|
-
from click import ClickException
|
|
8
7
|
from cmem.cmempy.config import get_keycloak_base_uri, get_keycloak_realm_id
|
|
9
8
|
from cmem.cmempy.keycloak.group import list_groups
|
|
10
9
|
from cmem.cmempy.keycloak.user import (
|
|
@@ -23,8 +22,10 @@ from cmem.cmempy.keycloak.user import (
|
|
|
23
22
|
from cmem_cmemc import completion
|
|
24
23
|
from cmem_cmemc.command import CmemcCommand
|
|
25
24
|
from cmem_cmemc.command_group import CmemcGroup
|
|
26
|
-
from cmem_cmemc.context import ApplicationContext
|
|
25
|
+
from cmem_cmemc.context import ApplicationContext, build_caption
|
|
26
|
+
from cmem_cmemc.exceptions import CmemcError
|
|
27
27
|
from cmem_cmemc.object_list import (
|
|
28
|
+
DirectMultiValuePropertyFilter,
|
|
28
29
|
DirectValuePropertyFilter,
|
|
29
30
|
ObjectList,
|
|
30
31
|
compare_regex,
|
|
@@ -71,10 +72,61 @@ user_list = ObjectList(
|
|
|
71
72
|
compare=compare_regex,
|
|
72
73
|
fixed_completion=[],
|
|
73
74
|
),
|
|
75
|
+
DirectMultiValuePropertyFilter(
|
|
76
|
+
name="usernames",
|
|
77
|
+
description="Internal filter for multiple usernames.",
|
|
78
|
+
property_key="username",
|
|
79
|
+
),
|
|
74
80
|
],
|
|
75
81
|
)
|
|
76
82
|
|
|
77
83
|
|
|
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
|
+
|
|
78
130
|
@click.command(cls=CmemcCommand, name="list")
|
|
79
131
|
@click.option("--raw", is_flag=True, help="Outputs raw JSON.")
|
|
80
132
|
@click.option(
|
|
@@ -117,33 +169,81 @@ def list_command(
|
|
|
117
169
|
)
|
|
118
170
|
for usr in users
|
|
119
171
|
]
|
|
172
|
+
filtered = len(filter_) > 0
|
|
120
173
|
app.echo_info_table(
|
|
121
174
|
table,
|
|
122
175
|
headers=["Username", "First Name", "Last Name", "Email"],
|
|
123
176
|
sort_column=0,
|
|
124
|
-
|
|
125
|
-
"
|
|
177
|
+
caption=build_caption(len(table), "user", filtered=filtered),
|
|
178
|
+
empty_table_message="No user accounts found for these filters."
|
|
179
|
+
if filtered
|
|
180
|
+
else "No user accounts found. Use the `admin user create` command to create an account.",
|
|
126
181
|
)
|
|
127
182
|
|
|
128
183
|
|
|
129
184
|
@click.command(cls=CmemcCommand, name="delete")
|
|
130
|
-
@click.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
""
|
|
185
|
+
@click.option(
|
|
186
|
+
"-a",
|
|
187
|
+
"--all",
|
|
188
|
+
"all_",
|
|
189
|
+
is_flag=True,
|
|
190
|
+
help="Delete all user accounts. This is a dangerous option, so use it with care.",
|
|
191
|
+
)
|
|
192
|
+
@click.option(
|
|
193
|
+
"--filter",
|
|
194
|
+
"filter_",
|
|
195
|
+
type=(str, str),
|
|
196
|
+
multiple=True,
|
|
197
|
+
shell_complete=user_list.complete_values,
|
|
198
|
+
help=user_list.get_filter_help_text(),
|
|
199
|
+
)
|
|
200
|
+
@click.argument("usernames", nargs=-1, type=click.STRING, shell_complete=completion.user_ids)
|
|
201
|
+
@click.pass_context
|
|
202
|
+
def delete_command(
|
|
203
|
+
ctx: click.Context,
|
|
204
|
+
all_: bool,
|
|
205
|
+
filter_: tuple[tuple[str, str]],
|
|
206
|
+
usernames: tuple[str],
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Delete user accounts.
|
|
209
|
+
|
|
210
|
+
This command deletes user accounts from a realm.
|
|
134
211
|
|
|
135
|
-
|
|
212
|
+
Warning: User accounts will be deleted without prompting.
|
|
136
213
|
|
|
137
|
-
Note: The deletion of
|
|
138
|
-
|
|
214
|
+
Note: The deletion of user accounts does not delete the assigned groups,
|
|
215
|
+
only the assignments to these groups. User accounts can be listed by using
|
|
216
|
+
the `admin user list` command.
|
|
139
217
|
"""
|
|
140
|
-
app
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
218
|
+
app = ctx.obj
|
|
219
|
+
|
|
220
|
+
# Validation: require at least one selection method
|
|
221
|
+
if not usernames and not all_ and not filter_:
|
|
222
|
+
raise click.UsageError(
|
|
223
|
+
"Either specify at least one username"
|
|
224
|
+
" or use a --filter option,"
|
|
225
|
+
" or use the --all option to delete all user accounts."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if usernames and (all_ or filter_):
|
|
229
|
+
raise click.UsageError("Either specify a username OR use a --filter or the --all option.")
|
|
230
|
+
|
|
231
|
+
# Get users to delete based on selection method
|
|
232
|
+
users_to_delete = _get_users_to_delete(ctx, usernames, all_, filter_)
|
|
233
|
+
|
|
234
|
+
# Avoid double removal as well as sort usernames
|
|
235
|
+
processed_usernames = sorted(set(users_to_delete), key=lambda v: v.lower())
|
|
236
|
+
count = len(processed_usernames)
|
|
144
237
|
|
|
145
|
-
|
|
146
|
-
|
|
238
|
+
# Delete each user
|
|
239
|
+
for current, username in enumerate(processed_usernames, start=1):
|
|
240
|
+
current_string = str(current).zfill(len(str(count)))
|
|
241
|
+
app.echo_info(f"Delete user {current_string}/{count}: {username} ... ", nl=False)
|
|
242
|
+
users = get_user_by_username(username)
|
|
243
|
+
if not users:
|
|
244
|
+
raise CmemcError(NO_USER_ERROR.format(username))
|
|
245
|
+
delete_user(users[0]["id"])
|
|
246
|
+
app.echo_success("deleted")
|
|
147
247
|
|
|
148
248
|
|
|
149
249
|
@click.command(cls=CmemcCommand, name="create")
|
|
@@ -161,7 +261,7 @@ def create_command(app: ApplicationContext, username: str) -> None:
|
|
|
161
261
|
app.echo_info(f"Creating user {username} ... ", nl=False)
|
|
162
262
|
users = get_user_by_username(username)
|
|
163
263
|
if users:
|
|
164
|
-
raise
|
|
264
|
+
raise CmemcError(EXISTING_USER_ERROR.format(username))
|
|
165
265
|
|
|
166
266
|
create_user(username=username)
|
|
167
267
|
app.echo_success("done")
|
|
@@ -215,14 +315,14 @@ def update_command( # noqa: PLR0913
|
|
|
215
315
|
app.echo_info(f"Updating user {username} ... ", nl=False)
|
|
216
316
|
users = get_user_by_username(username)
|
|
217
317
|
if not users:
|
|
218
|
-
raise
|
|
318
|
+
raise CmemcError(NO_USER_ERROR.format(username))
|
|
219
319
|
user_id = users[0]["id"]
|
|
220
320
|
all_groups = {group["name"]: group["id"] for group in list_groups()}
|
|
221
321
|
invalid_groups = [group for group in assign_group if group not in all_groups]
|
|
222
322
|
existing_user_groups = {group["name"] for group in user_groups(user_id)}
|
|
223
323
|
|
|
224
324
|
if invalid_groups:
|
|
225
|
-
raise
|
|
325
|
+
raise CmemcError(
|
|
226
326
|
NO_GROUP_ERROR.format(
|
|
227
327
|
", ".join(invalid_groups), ", ".join(all_groups.keys() - set(existing_user_groups))
|
|
228
328
|
)
|
|
@@ -232,7 +332,7 @@ def update_command( # noqa: PLR0913
|
|
|
232
332
|
group for group in unassign_group if group not in existing_user_groups
|
|
233
333
|
]
|
|
234
334
|
if invalid_unassign_groups:
|
|
235
|
-
raise
|
|
335
|
+
raise CmemcError(
|
|
236
336
|
INVALID_UNASSIGN_GROUP_ERROR.format(
|
|
237
337
|
", ".join(invalid_unassign_groups), ", ".join(existing_user_groups)
|
|
238
338
|
)
|
|
@@ -287,7 +387,7 @@ def password_command(
|
|
|
287
387
|
app.echo_info(f"Changing password for account {username} ... ", nl=False)
|
|
288
388
|
users = get_user_by_username(username)
|
|
289
389
|
if not users:
|
|
290
|
-
raise
|
|
390
|
+
raise CmemcError(NO_USER_ERROR.format(username))
|
|
291
391
|
if not value and not request_change:
|
|
292
392
|
app.echo_info("\nNew password: ", nl=False)
|
|
293
393
|
value = getpass(prompt="")
|
|
@@ -300,7 +400,7 @@ def password_command(
|
|
|
300
400
|
if value:
|
|
301
401
|
reset_password(user_id=users[0]["id"], value=value, temporary=temporary)
|
|
302
402
|
if request_change and not users[0].get("email", None):
|
|
303
|
-
raise
|
|
403
|
+
raise CmemcError(NO_EMAIL_ERROR.format(username))
|
|
304
404
|
if request_change:
|
|
305
405
|
request_password_change(users[0]["id"])
|
|
306
406
|
app.echo_success("done")
|
|
@@ -331,7 +431,7 @@ def open_command(app: ApplicationContext, usernames: str) -> None:
|
|
|
331
431
|
user_name_id_map = {u["username"]: u["id"] for u in users}
|
|
332
432
|
for _ in usernames:
|
|
333
433
|
if _ not in user_name_id_map:
|
|
334
|
-
raise
|
|
434
|
+
raise CmemcError(NO_USER_ERROR.format(_))
|
|
335
435
|
user_id = user_name_id_map[_]
|
|
336
436
|
open_user_uri = f"{open_user_base_uri}/{user_id}/settings"
|
|
337
437
|
|
|
@@ -8,19 +8,18 @@ from datetime import datetime, timezone
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
import click
|
|
11
|
-
import timeago
|
|
12
11
|
from click import Context, UsageError
|
|
13
12
|
from click.shell_completion import CompletionItem
|
|
14
|
-
from cmem.cmempy.config import get_cmem_base_uri
|
|
15
13
|
from cmem.cmempy.dp.shacl import validation
|
|
14
|
+
from humanize import naturaltime
|
|
16
15
|
from junit_xml import TestCase, TestSuite, to_xml_report_string
|
|
17
16
|
from rich.progress import Progress, SpinnerColumn, TaskID, TimeElapsedColumn
|
|
18
17
|
|
|
19
18
|
from cmem_cmemc import completion
|
|
20
19
|
from cmem_cmemc.command import CmemcCommand
|
|
21
20
|
from cmem_cmemc.command_group import CmemcGroup
|
|
22
|
-
from cmem_cmemc.completion import finalize_completion
|
|
23
|
-
from cmem_cmemc.context import ApplicationContext
|
|
21
|
+
from cmem_cmemc.completion import finalize_completion, suppress_completion_errors
|
|
22
|
+
from cmem_cmemc.context import ApplicationContext, build_caption
|
|
24
23
|
from cmem_cmemc.exceptions import ServerError
|
|
25
24
|
from cmem_cmemc.object_list import (
|
|
26
25
|
DirectListPropertyFilter,
|
|
@@ -213,7 +212,7 @@ def _get_batch_validation_option(validation_: dict) -> tuple[str, str]:
|
|
|
213
212
|
state = validation_["state"]
|
|
214
213
|
graph = validation_["contextGraphIri"]
|
|
215
214
|
stamp = datetime.fromtimestamp(validation_["executionStarted"] / 1000, tz=timezone.utc)
|
|
216
|
-
time_ago =
|
|
215
|
+
time_ago = naturaltime(stamp, when=datetime.now(tz=timezone.utc))
|
|
217
216
|
resources = _get_resource_count(validation_)
|
|
218
217
|
violations = _get_violation_count(validation_)
|
|
219
218
|
return (
|
|
@@ -222,6 +221,7 @@ def _get_batch_validation_option(validation_: dict) -> tuple[str, str]:
|
|
|
222
221
|
)
|
|
223
222
|
|
|
224
223
|
|
|
224
|
+
@suppress_completion_errors
|
|
225
225
|
def _complete_all_batch_validations(
|
|
226
226
|
ctx: click.Context, # noqa: ARG001
|
|
227
227
|
param: click.Argument, # noqa: ARG001
|
|
@@ -232,6 +232,7 @@ def _complete_all_batch_validations(
|
|
|
232
232
|
return finalize_completion(candidates=options, incomplete=incomplete)
|
|
233
233
|
|
|
234
234
|
|
|
235
|
+
@suppress_completion_errors
|
|
235
236
|
def _complete_running_batch_validations(
|
|
236
237
|
ctx: click.Context, # noqa: ARG001
|
|
237
238
|
param: click.Argument, # noqa: ARG001
|
|
@@ -246,6 +247,7 @@ def _complete_running_batch_validations(
|
|
|
246
247
|
return finalize_completion(candidates=options, incomplete=incomplete)
|
|
247
248
|
|
|
248
249
|
|
|
250
|
+
@suppress_completion_errors
|
|
249
251
|
def _complete_finished_batch_validations(
|
|
250
252
|
ctx: click.Context, # noqa: ARG001
|
|
251
253
|
param: click.Argument, # noqa: ARG001
|
|
@@ -330,10 +332,10 @@ def _wait_for_process_completion(
|
|
|
330
332
|
progress.stop()
|
|
331
333
|
progress.__exit__(None, None, None)
|
|
332
334
|
if state.status == validation.STATUS_CANCELLED:
|
|
333
|
-
raise ServerError(
|
|
335
|
+
raise ServerError("Process was cancelled.")
|
|
334
336
|
if state.status == validation.STATUS_ERROR:
|
|
335
337
|
error_message = state.data.get("error", "")
|
|
336
|
-
raise ServerError(
|
|
338
|
+
raise ServerError(f"Process ended with error: {error_message}")
|
|
337
339
|
return state.status
|
|
338
340
|
|
|
339
341
|
|
|
@@ -579,13 +581,16 @@ def list_command(ctx: Context, filter_: tuple[tuple[str, str]], id_only: bool, r
|
|
|
579
581
|
_get_violation_count(_),
|
|
580
582
|
]
|
|
581
583
|
table.append(row)
|
|
584
|
+
filtered = len(filter_) > 0
|
|
582
585
|
app.echo_info_table(
|
|
583
586
|
table,
|
|
584
587
|
headers=["ID", "Status", "Started", "Graph", "Resources", "Violations"],
|
|
585
|
-
caption=
|
|
588
|
+
caption=build_caption(len(table), "validation process", filtered=filtered),
|
|
586
589
|
cell_processing={2: TimeAgo(), 3: GraphLink()},
|
|
587
|
-
empty_table_message="No validation processes found.
|
|
588
|
-
|
|
590
|
+
empty_table_message="No validation processes found for these filters."
|
|
591
|
+
if filtered
|
|
592
|
+
else "No validation processes found."
|
|
593
|
+
" Use `graph validation execute` to start a new validation process.",
|
|
589
594
|
)
|
|
590
595
|
|
|
591
596
|
|