midpoint-cli 1.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. midpoint_cli/__init__.py +234 -0
  2. midpoint_cli/client/__init__.py +199 -0
  3. midpoint_cli/client/audit.py +178 -0
  4. midpoint_cli/client/audit_query.py +247 -0
  5. midpoint_cli/client/configuration.py +15 -0
  6. midpoint_cli/client/constants.py +37 -0
  7. midpoint_cli/client/errors.py +74 -0
  8. midpoint_cli/client/logging_config.py +122 -0
  9. midpoint_cli/client/midpoint_client.py +866 -0
  10. midpoint_cli/client/object_query.py +230 -0
  11. midpoint_cli/client/objects.py +814 -0
  12. midpoint_cli/client/observer.py +29 -0
  13. midpoint_cli/client/patch.py +113 -0
  14. midpoint_cli/client/progress.py +80 -0
  15. midpoint_cli/client/rest.py +490 -0
  16. midpoint_cli/client/safe_xml.py +24 -0
  17. midpoint_cli/client/server_info.py +130 -0
  18. midpoint_cli/client/server_version.py +74 -0
  19. midpoint_cli/client/session.py +142 -0
  20. midpoint_cli/client/task_definitions.py +110 -0
  21. midpoint_cli/prompt/__init__.py +196 -0
  22. midpoint_cli/prompt/audit.py +253 -0
  23. midpoint_cli/prompt/base.py +218 -0
  24. midpoint_cli/prompt/complete.py +291 -0
  25. midpoint_cli/prompt/configuration.py +258 -0
  26. midpoint_cli/prompt/connector.py +45 -0
  27. midpoint_cli/prompt/console.py +818 -0
  28. midpoint_cli/prompt/delete.py +89 -0
  29. midpoint_cli/prompt/get.py +66 -0
  30. midpoint_cli/prompt/info.py +53 -0
  31. midpoint_cli/prompt/log.py +98 -0
  32. midpoint_cli/prompt/org.py +122 -0
  33. midpoint_cli/prompt/output.py +33 -0
  34. midpoint_cli/prompt/put.py +71 -0
  35. midpoint_cli/prompt/report.py +69 -0
  36. midpoint_cli/prompt/resource.py +217 -0
  37. midpoint_cli/prompt/role.py +96 -0
  38. midpoint_cli/prompt/script.py +154 -0
  39. midpoint_cli/prompt/search.py +51 -0
  40. midpoint_cli/prompt/shadow.py +87 -0
  41. midpoint_cli/prompt/task.py +296 -0
  42. midpoint_cli/prompt/user.py +406 -0
  43. midpoint_cli-1.6.0.dist-info/METADATA +1334 -0
  44. midpoint_cli-1.6.0.dist-info/RECORD +47 -0
  45. midpoint_cli-1.6.0.dist-info/WHEEL +4 -0
  46. midpoint_cli-1.6.0.dist-info/entry_points.txt +3 -0
  47. midpoint_cli-1.6.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,296 @@
1
+ from argparse import ArgumentParser, RawTextHelpFormatter
2
+ from typing import Any, cast
3
+
4
+ # Task command wrapper parser
5
+ from midpoint_cli.client import (
6
+ MidpointObjectList,
7
+ MidpointObjectTypes,
8
+ MidpointTask,
9
+ NopProgressMonitor,
10
+ TaskExecutionFailure,
11
+ )
12
+ from midpoint_cli.client.constants import ResultStatus, TaskExecutionState
13
+ from midpoint_cli.client.object_query import TASK_STATUS_FILTERS
14
+ from midpoint_cli.client.safe_xml import fromstring as safe_fromstring
15
+ from midpoint_cli.prompt.base import PromptBase, add_listing_arguments
16
+ from midpoint_cli.prompt.complete import TASK, completes
17
+ from midpoint_cli.prompt.console import (
18
+ OUTCOME_STYLE,
19
+ create_progress_monitor,
20
+ get_console,
21
+ is_machine_output,
22
+ print_details,
23
+ print_error,
24
+ print_info,
25
+ print_rows,
26
+ )
27
+
28
+ task_parser = ArgumentParser(
29
+ formatter_class=RawTextHelpFormatter,
30
+ prog='task',
31
+ description='Manage server tasks.',
32
+ epilog="""
33
+ Available commands:
34
+ ls List all server tasks.
35
+ get Show the details of task(s).
36
+ run Run task(s) sequentially.
37
+ suspend Suspend task(s).
38
+ resume Resume task(s).
39
+ wait Wait for task completion.
40
+ """,
41
+ )
42
+ task_parser.add_argument('command', help='Task command to execute.')
43
+ task_parser.add_argument('arg', help='Optional command arguments.', nargs='*')
44
+
45
+ # Task LS parser
46
+
47
+ task_ls_parser = add_listing_arguments(
48
+ ArgumentParser(
49
+ formatter_class=RawTextHelpFormatter,
50
+ prog='task ls',
51
+ description='List server tasks.',
52
+ ),
53
+ 'tasks',
54
+ )
55
+ task_ls_parser.add_argument(
56
+ '--status',
57
+ action='append',
58
+ choices=list(TASK_STATUS_FILTERS),
59
+ metavar='STATUS',
60
+ help='Only list tasks in this status (filtered on the server); repeat to list several.\n'
61
+ 'Execution states: running, runnable, waiting, suspended, closed.\n'
62
+ 'Last run results: in_progress, success, warning, failed (fatal or partial error).',
63
+ )
64
+
65
+ # Task GET parser
66
+
67
+ task_get_parser = ArgumentParser(
68
+ formatter_class=RawTextHelpFormatter,
69
+ prog='task get',
70
+ description='Show the details of tasks.',
71
+ )
72
+ completes(task_get_parser.add_argument('task', help='Task to show. Can be an OID or a task name.', nargs='+'), TASK)
73
+
74
+ # Task RUN parser
75
+
76
+ task_run_parser = ArgumentParser(
77
+ formatter_class=RawTextHelpFormatter,
78
+ prog='task run',
79
+ description='Run tasks synchronously.',
80
+ )
81
+ completes(task_run_parser.add_argument('task', help='Task to be run. Can be an OID or a task name.', nargs='+'), TASK)
82
+ task_run_parser.add_argument(
83
+ '--no-changes',
84
+ action='store_true',
85
+ help="Don't stream audit events as the task runs (silences the inline change log).",
86
+ )
87
+
88
+ # Task WAIT parser
89
+
90
+ task_wait_parser = ArgumentParser(
91
+ formatter_class=RawTextHelpFormatter,
92
+ prog='task wait',
93
+ description='Wait for tasks to complete. Without a task, waits for every task in progress.',
94
+ )
95
+ completes(
96
+ task_wait_parser.add_argument('task', help='Task to wait for. Can be an OID or a task name.', nargs='*'), TASK
97
+ )
98
+ task_wait_parser.add_argument(
99
+ '--no-changes',
100
+ action='store_true',
101
+ help="Don't stream audit events while waiting (silences the inline change log).",
102
+ )
103
+
104
+ # Task SUSPEND and RESUME parsers
105
+
106
+ task_suspend_parser = ArgumentParser(
107
+ formatter_class=RawTextHelpFormatter,
108
+ prog='task suspend',
109
+ description='Suspend tasks.',
110
+ )
111
+ completes(
112
+ task_suspend_parser.add_argument('task', help='Task to suspend. Can be an OID or a task name.', nargs='+'), TASK
113
+ )
114
+
115
+ task_resume_parser = ArgumentParser(
116
+ formatter_class=RawTextHelpFormatter,
117
+ prog='task resume',
118
+ description='Resume suspended tasks.',
119
+ )
120
+ completes(
121
+ task_resume_parser.add_argument('task', help='Task to resume. Can be an OID or a task name.', nargs='+'), TASK
122
+ )
123
+
124
+ # Each subcommand with its parser; None for those taking no arguments.
125
+ task_subparsers: dict[str, ArgumentParser | None] = {
126
+ 'ls': task_ls_parser,
127
+ 'get': task_get_parser,
128
+ 'run': task_run_parser,
129
+ 'wait': task_wait_parser,
130
+ 'suspend': task_suspend_parser,
131
+ 'resume': task_resume_parser,
132
+ }
133
+
134
+
135
+ def _task_cell_style(column: str, value: str) -> str | None:
136
+ """Color the result status of a task row by outcome; an empty status keeps the column style."""
137
+ if column == 'Result Status' and value:
138
+ return OUTCOME_STYLE.get(value.replace('-', '_').lower())
139
+ return None
140
+
141
+
142
+ def task_details(task: MidpointTask) -> dict[str, Any]:
143
+ """A task as one detail object: the properties of `task get`, then its Transitions and Actions.
144
+
145
+ The nested records use the column names of the Transitions and Actions tables.
146
+ """
147
+ details: dict[str, Any] = dict(task)
148
+ details['Transitions'] = [
149
+ {'From State': from_state, 'To State': to_state, 'Count': count}
150
+ for from_state, to_state, count in task.get_transitions()
151
+ ]
152
+ details['Actions'] = [
153
+ {'Type': object_type, 'Action': operation, 'Success': success, 'Failure': failure}
154
+ for object_type, operation, success, failure in task.get_actions()
155
+ ]
156
+ return details
157
+
158
+
159
+ class TaskClientPrompt(PromptBase):
160
+ subparsers_task = task_subparsers
161
+
162
+ def do_task(self, inp: str) -> None:
163
+ """Manage server tasks (subcommands: ls, run, suspend, resume, wait, get)."""
164
+ # Strict: an unknown subcommand followed by arguments is a usage error, so a typo
165
+ # in a subcommand is not mistaken for another command.
166
+ self._dispatch_subcommand(
167
+ inp,
168
+ task_parser,
169
+ task_subparsers,
170
+ {
171
+ 'ls': self._task_ls,
172
+ 'get': self._task_get,
173
+ 'wait': self._task_wait,
174
+ 'run': lambda args: self._task_action('run', task_run_parser, args),
175
+ 'resume': lambda args: self._task_action('resume', task_resume_parser, args),
176
+ 'suspend': lambda args: self._task_action('suspend', task_suspend_parser, args),
177
+ },
178
+ strict=True,
179
+ )
180
+
181
+ def _task_ls(self, args: list[str]) -> None:
182
+ ls_ns = task_ls_parser.parse_args(args)
183
+ if ls_ns.status:
184
+ tasks = self.client.get_tasks(limit=ls_ns.limit, name_filter=ls_ns.filter, statuses=ls_ns.status)
185
+ else:
186
+ tasks = self.client.get_tasks(limit=ls_ns.limit, name_filter=ls_ns.filter)
187
+ self._print_tasks_table(tasks)
188
+
189
+ def _task_get(self, args: list[str]) -> None:
190
+ get_ns = task_get_parser.parse_args(args)
191
+ details = []
192
+
193
+ for task_id in get_ns.task:
194
+ task_obj = self._find_reference(MidpointObjectTypes.TASK.value, task_id, 'Task')
195
+ if task_obj is None:
196
+ continue
197
+ if is_machine_output():
198
+ details.append(task_details(cast(MidpointTask, task_obj)))
199
+ else:
200
+ get_console().print(cast(MidpointTask, task_obj).get_full_description())
201
+
202
+ if is_machine_output():
203
+ # Shaped by the arguments, not by how many were found: one task, one object.
204
+ print_details(details, as_list=len(get_ns.task) > 1)
205
+
206
+ def _task_wait(self, args: list[str]) -> None:
207
+ wait_ns = task_wait_parser.parse_args(args)
208
+ tasks_to_wait: list[str] = wait_ns.task or [
209
+ oid
210
+ for task in self.client.get_tasks()
211
+ if task['Result Status'] == ResultStatus.IN_PROGRESS and (oid := task.get_oid()) is not None
212
+ ]
213
+
214
+ for task_id in tasks_to_wait:
215
+ task_obj = self._find_reference(MidpointObjectTypes.TASK.value, task_id, 'Task')
216
+ if task_obj is None:
217
+ continue
218
+ task_oid = task_obj.get_oid()
219
+ if task_oid is None:
220
+ raise AssertionError('Task OID cannot be None')
221
+ print_info(f'Waiting for task {task_oid} / {task_obj.get_name()}')
222
+ try:
223
+ self.client.task_wait(task_oid, create_progress_monitor(), show_changes=not wait_ns.no_changes)
224
+ except TaskExecutionFailure as e:
225
+ # Reported now, so a later failure does not hide this one.
226
+ self._fail()
227
+ self._print_failed_task(task_oid, e)
228
+ print_error(e.message)
229
+
230
+ def _task_action(self, action: str, parser: ArgumentParser, args: list[str]) -> None:
231
+ """task run/resume/suspend: apply the action to each task in turn, stopping at the first failure."""
232
+ action_ns = parser.parse_args(args)
233
+
234
+ for task_id in action_ns.task:
235
+ task_obj = self._find_reference(MidpointObjectTypes.TASK.value, task_id, 'Task')
236
+ if task_obj is None:
237
+ continue
238
+ task_oid = task_obj.get_oid()
239
+ if task_oid is None:
240
+ raise AssertionError('Task OID cannot be None')
241
+ if action == 'run' and task_obj['Execution Status'] == TaskExecutionState.SUSPENDED:
242
+ print_info('Task currently suspended, activating it...')
243
+ self.client.task_action(task_oid, 'resume', NopProgressMonitor())
244
+ print_info('Now running task...')
245
+
246
+ print_info(f'Task {task_obj.get_name()} - {action}')
247
+ # Only a run is followed to completion.
248
+ monitor = create_progress_monitor() if action == 'run' else NopProgressMonitor()
249
+
250
+ try:
251
+ task_result = self.client.task_action(
252
+ task_oid,
253
+ action,
254
+ monitor,
255
+ show_changes=not getattr(action_ns, 'no_changes', False),
256
+ )
257
+ # Always display the task result if available
258
+ if task_result is not None:
259
+ get_console().print(task_result.get_full_description())
260
+ except TaskExecutionFailure as e:
261
+ self._print_failed_task(task_oid, e)
262
+ self._fail(e.message)
263
+ break
264
+
265
+ def _print_failed_task(self, task_oid: str, failure: TaskExecutionFailure) -> None:
266
+ """Show the details and statistics of a failed task, before its error is reported.
267
+
268
+ The error and action counts are what tells why a task failed, so they are shown
269
+ exactly as for a successful run. The task comes with the failure when the client
270
+ read it while waiting; otherwise it is fetched again.
271
+ """
272
+ failed_task = failure.task
273
+ if failed_task is None:
274
+ try:
275
+ failed_task_xml = self.client.api_client.get_element(MidpointObjectTypes.TASK.value, task_oid)
276
+ failed_task = MidpointTask(safe_fromstring(failed_task_xml), self.client.get_server_version())
277
+ except Exception:
278
+ return # Without the task details, the error is still reported
279
+ get_console().print(failed_task.get_full_description())
280
+
281
+ @staticmethod
282
+ def help_task() -> None:
283
+ task_parser.print_help()
284
+
285
+ def do_tasks(self, inp: str) -> None:
286
+ """Shortcut for 'task ls' — list all server tasks."""
287
+ self._list_shortcut(inp, 'tasks', 'task', self.do_task)
288
+
289
+ @staticmethod
290
+ def help_tasks() -> None:
291
+ print('List all server tasks. This is a shortcut for "task ls"')
292
+
293
+ @staticmethod
294
+ def _print_tasks_table(tasks: MidpointObjectList) -> None:
295
+ """Print tasks in the selected output format, coloring the result status."""
296
+ print_rows(tasks, title='Server Tasks', noun='tasks', cell_style=_task_cell_style)