Merge ~sylvain-pineau/checkbox-ng:urwid_manifest_v2 into checkbox-ng:master
- Git
- lp:~sylvain-pineau/checkbox-ng
- urwid_manifest_v2
- Merge into master
Proposed by
Sylvain Pineau
Status: | Merged |
---|---|
Approved by: | Sylvain Pineau |
Approved revision: | 2300945a3d5e550002da60e1c0ca1e2a098a1cad |
Merged at revision: | a73387bcd71cd8cb03c5fbaf2a207c6fdc0acb40 |
Proposed branch: | ~sylvain-pineau/checkbox-ng:urwid_manifest_v2 |
Merge into: | checkbox-ng:master |
Diff against target: |
546 lines (+344/-3) 8 files modified
checkbox_ng/launcher/master.py (+14/-1) checkbox_ng/launcher/subcommands.py (+12/-0) checkbox_ng/urwid_ui.py (+131/-0) docs/launcher-tutorial.rst (+20/-0) plainbox/impl/launcher.py (+2/-0) plainbox/impl/resource.py (+54/-0) plainbox/impl/session/assistant.py (+104/-1) plainbox/impl/session/remote_assistant.py (+7/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Sylvain Pineau (community) | Approve | ||
Review via email: mp+375845@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/checkbox_ng/launcher/master.py b/checkbox_ng/launcher/master.py |
2 | index 5c9afcf..2107fbf 100644 |
3 | --- a/checkbox_ng/launcher/master.py |
4 | +++ b/checkbox_ng/launcher/master.py |
5 | @@ -36,11 +36,13 @@ from tempfile import SpooledTemporaryFile |
6 | |
7 | from plainbox.impl.color import Colorizer |
8 | from plainbox.impl.launcher import DefaultLauncherDefinition |
9 | +from plainbox.impl.secure.config import Unset |
10 | from plainbox.impl.secure.sudo_broker import SudoProvider |
11 | from plainbox.impl.session.remote_assistant import RemoteSessionAssistant |
12 | from plainbox.vendor import rpyc |
13 | from checkbox_ng.urwid_ui import TestPlanBrowser |
14 | from checkbox_ng.urwid_ui import CategoryBrowser |
15 | +from checkbox_ng.urwid_ui import ManifestBrowser |
16 | from checkbox_ng.urwid_ui import ReRunBrowser |
17 | from checkbox_ng.urwid_ui import interrupt_dialog |
18 | from checkbox_ng.urwid_ui import resume_dialog |
19 | @@ -284,7 +286,13 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
20 | self.jobs = self.sa.finish_bootstrap() |
21 | |
22 | def select_jobs(self, all_jobs): |
23 | - if not self.launcher.test_selection_forced: |
24 | + if self.launcher.test_selection_forced: |
25 | + if self.launcher.manifest is not Unset: |
26 | + self.sa.save_manifest( |
27 | + {manifest_id: self.launcher.manifest[manifest_id] for |
28 | + manifest_id in self.launcher.manifest} |
29 | + ) |
30 | + else: |
31 | _logger.info("master: Selecting jobs.") |
32 | reprs = self.sa.get_jobs_repr(all_jobs) |
33 | wanted_set = CategoryBrowser( |
34 | @@ -296,6 +304,11 @@ class RemoteMaster(ReportsStage, MainLoopStage): |
35 | chosen_jobs = [job for job in all_jobs if job in wanted_set] |
36 | _logger.debug("master: Selected jobs: %s", chosen_jobs) |
37 | self.sa.modify_todo_list(chosen_jobs) |
38 | + manifest_repr = self.sa.get_manifest_repr() |
39 | + if manifest_repr: |
40 | + manifest_answers = ManifestBrowser( |
41 | + "System Manifest:", manifest_repr).run() |
42 | + self.sa.save_manifest(manifest_answers) |
43 | self.sa.finish_job_selection() |
44 | self.run_jobs() |
45 | |
46 | diff --git a/checkbox_ng/launcher/subcommands.py b/checkbox_ng/launcher/subcommands.py |
47 | index 2a33b13..75e6770 100644 |
48 | --- a/checkbox_ng/launcher/subcommands.py |
49 | +++ b/checkbox_ng/launcher/subcommands.py |
50 | @@ -47,6 +47,7 @@ from plainbox.impl.providers import get_providers |
51 | from plainbox.impl.providers.embedded_providers import ( |
52 | EmbeddedProvider1PlugInCollection) |
53 | from plainbox.impl.result import MemoryJobResult |
54 | +from plainbox.impl.secure.config import Unset |
55 | from plainbox.impl.secure.sudo_broker import sudo_password_provider |
56 | from plainbox.impl.session.assistant import SessionAssistant, SA_RESTARTABLE |
57 | from plainbox.impl.session.jobs import InhibitionCause |
58 | @@ -64,6 +65,7 @@ from checkbox_ng.launcher.startprovider import ( |
59 | from checkbox_ng.launcher.run import Action |
60 | from checkbox_ng.launcher.run import NormalUI |
61 | from checkbox_ng.urwid_ui import CategoryBrowser |
62 | +from checkbox_ng.urwid_ui import ManifestBrowser |
63 | from checkbox_ng.urwid_ui import ReRunBrowser |
64 | from checkbox_ng.urwid_ui import TestPlanBrowser |
65 | |
66 | @@ -401,6 +403,11 @@ class Launcher(MainLoopStage, ReportsStage): |
67 | |
68 | def _pick_jobs_to_run(self): |
69 | if self.launcher.test_selection_forced: |
70 | + if self.launcher.manifest is not Unset: |
71 | + self.ctx.sa.save_manifest( |
72 | + {manifest_id: self.launcher.manifest[manifest_id] for |
73 | + manifest_id in self.launcher.manifest} |
74 | + ) |
75 | # by default all tests are selected; so we're done here |
76 | return |
77 | job_list = [self.ctx.sa.get_job(job_id) for job_id in |
78 | @@ -411,6 +418,11 @@ class Launcher(MainLoopStage, ReportsStage): |
79 | test_info_list = self._generate_job_infos(job_list) |
80 | wanted_set = CategoryBrowser( |
81 | _("Choose tests to run on your system:"), test_info_list).run() |
82 | + manifest_repr = self.ctx.sa.get_manifest_repr() |
83 | + if manifest_repr: |
84 | + manifest_answers = ManifestBrowser( |
85 | + "System Manifest:", manifest_repr).run() |
86 | + self.ctx.sa.save_manifest(manifest_answers) |
87 | # no need to set an alternate selection if the job list not changed |
88 | if len(test_info_list) == len(wanted_set): |
89 | return |
90 | diff --git a/checkbox_ng/urwid_ui.py b/checkbox_ng/urwid_ui.py |
91 | index 569e0df..2d5b579 100644 |
92 | --- a/checkbox_ng/urwid_ui.py |
93 | +++ b/checkbox_ng/urwid_ui.py |
94 | @@ -744,6 +744,137 @@ class CountdownWidget(urwid.BigText): |
95 | raise urwid.ExitMainLoop |
96 | |
97 | |
98 | +class ManifestNaturalEdit(urwid.IntEdit): |
99 | + |
100 | + def keypress(self, size, key): |
101 | + (maxcol,) = size |
102 | + return urwid.Edit.keypress(self, (maxcol,), key) |
103 | + |
104 | + def value(self): |
105 | + if self.edit_text: |
106 | + return int(self.edit_text) |
107 | + |
108 | + |
109 | +class ManifestQuestion(urwid.WidgetWrap): |
110 | + |
111 | + def __init__(self, question): |
112 | + self.id = question['id'] |
113 | + self._value = question['value'] |
114 | + self._value_type = question['value_type'] |
115 | + if self._value_type == 'bool': |
116 | + self.options = [] |
117 | + yes = urwid.RadioButton( |
118 | + self.options, "Yes", state=False, |
119 | + on_state_change=self._set_bool_value) |
120 | + no = urwid.RadioButton( |
121 | + self.options, "No", state=False, |
122 | + on_state_change=self._set_bool_value) |
123 | + if question['value'] is not None: |
124 | + if question['value'] is True: |
125 | + yes.set_state(True) |
126 | + else: |
127 | + no.set_state(True) |
128 | + self.display_widget = urwid.Columns([ |
129 | + urwid.Padding(urwid.Text(question['name']), left=2), |
130 | + urwid.GridFlow([yes, no], 7, 3, 1, align='left') |
131 | + ], dividechars=5) |
132 | + urwid.WidgetWrap.__init__(self, self.display_widget) |
133 | + elif self._value_type == 'natural': |
134 | + self._edit_widget = ManifestNaturalEdit(u"", self._value) |
135 | + self.display_widget = urwid.Columns([ |
136 | + urwid.Padding(urwid.Text(question['name']), left=2), |
137 | + (8, urwid.Padding(urwid.Text("["), left=7)), |
138 | + self._edit_widget, |
139 | + (1, urwid.Text("]")) |
140 | + ]) |
141 | + urwid.WidgetWrap.__init__(self, self.display_widget) |
142 | + |
143 | + def _set_bool_value(self, w, new_state, user_data=None): |
144 | + if w.label == 'Yes' and new_state: |
145 | + self._value = new_state |
146 | + elif w.label == 'No' and new_state: |
147 | + self._value = False |
148 | + |
149 | + @property |
150 | + def value(self): |
151 | + if self._value_type == 'bool': |
152 | + return self._value |
153 | + elif self._value_type == 'natural': |
154 | + return self._edit_widget.value() |
155 | + |
156 | + |
157 | +class ManifestBrowser: |
158 | + palette = [ |
159 | + ('body', 'light gray', 'black'), |
160 | + ('buttnf', 'black', 'light gray'), |
161 | + ('buttn', 'light gray', 'black', 'bold'), |
162 | + ('head', 'black', 'light gray', 'standout'), |
163 | + ('foot', 'light gray', 'black'), |
164 | + ('title', 'white', 'black', 'bold'), |
165 | + ('start', 'dark green,bold', 'black'), |
166 | + ('bold', 'bold', 'black'), |
167 | + ] |
168 | + |
169 | + footer_text = [('Press ('), ('start', 'T'), (') to start Testing')] |
170 | + footer_shortcuts = [('Shortcuts: '), ('bold', 'y'), ('/'), ('bold', 'n ')] |
171 | + |
172 | + def __init__(self, title, manifest): |
173 | + self.manifest = manifest |
174 | + self._manifest_out = {} |
175 | + self._widget_cache = [] |
176 | + # Header |
177 | + self.header = urwid.Padding(urwid.Text(title), left=1) |
178 | + # Body |
179 | + content = [] |
180 | + for prompt, questions in sorted(self.manifest.items()): |
181 | + content.append(urwid.Text(prompt)) |
182 | + for q in sorted(questions, key=lambda i: i['name']): |
183 | + question_widget = ManifestQuestion(q) |
184 | + content.append(urwid.AttrWrap(question_widget, |
185 | + 'buttn', 'buttnf')) |
186 | + self._widget_cache.append(question_widget) |
187 | + self._pile = urwid.Pile(content) |
188 | + listbox_content = [ |
189 | + urwid.Padding(self._pile, left=1, right=1, min_width=13), |
190 | + ] |
191 | + self.listbox = urwid.ListBox(urwid.SimpleListWalker(listbox_content)) |
192 | + # Footer |
193 | + self.default_footer = urwid.AttrWrap(urwid.Columns( |
194 | + [urwid.Padding(urwid.Text(self.footer_text), left=1), |
195 | + urwid.Text(self.footer_shortcuts, 'right')]), 'foot') |
196 | + # Main frame |
197 | + self.frame = urwid.Frame( |
198 | + urwid.AttrWrap(urwid.LineBox(self.listbox), 'body'), |
199 | + header=urwid.AttrWrap(self.header, 'head'), |
200 | + footer=self.default_footer) |
201 | + |
202 | + def run(self): |
203 | + """Run the urwid MainLoop.""" |
204 | + self.loop = urwid.MainLoop( |
205 | + self.frame, self.palette, unhandled_input=self.unhandled_input, |
206 | + handle_mouse=False) |
207 | + self.loop.run() |
208 | + for w in self._widget_cache: |
209 | + self._manifest_out.update({w.id: w.value}) |
210 | + return self._manifest_out |
211 | + |
212 | + def unhandled_input(self, key): |
213 | + if key in ('t', 'T'): |
214 | + for w in self._widget_cache: |
215 | + if w.value is None: |
216 | + break |
217 | + else: |
218 | + raise urwid.ExitMainLoop() |
219 | + if self._pile.focus._value_type == 'bool': |
220 | + if key in ('y', 'Y'): |
221 | + self.loop.process_input(["left", " ", "down"]) |
222 | + elif key in ('n', 'N'): |
223 | + self.loop.process_input(["right", " ", "down"]) |
224 | + elif self._pile.focus._value_type == 'natural': |
225 | + if key == 'enter': |
226 | + self.loop.process_input(["down"]) |
227 | + |
228 | + |
229 | def resume_dialog(duration): |
230 | palette = [ |
231 | ('body', 'light gray', 'black', 'standout'), |
232 | diff --git a/docs/launcher-tutorial.rst b/docs/launcher-tutorial.rst |
233 | index 81c8d41..5d50e48 100644 |
234 | --- a/docs/launcher-tutorial.rst |
235 | +++ b/docs/launcher-tutorial.rst |
236 | @@ -392,6 +392,26 @@ Checkbox-slave daemon is run by root so in order to run some jobs as an |
237 | unpriviledged user this variable can be used. |
238 | |
239 | |
240 | +Manifest section |
241 | +================ |
242 | + |
243 | +``[manifest]`` |
244 | + |
245 | +Beginning of the manifest section. |
246 | + |
247 | +Each variable present in the ``manifest`` section will be used a a preset value |
248 | +for the system manifest, taking precedence over the disk cache. |
249 | + |
250 | +Example: |
251 | + |
252 | +:: |
253 | + |
254 | + [manifest] |
255 | + com.canonical.certification::has_touchscreen = yes |
256 | + com.canonical.certification::has_usb_type_c = true |
257 | + com.canonical.certification::foo = 23 |
258 | + |
259 | + |
260 | Generating reports |
261 | ================== |
262 | |
263 | diff --git a/plainbox/impl/launcher.py b/plainbox/impl/launcher.py |
264 | index 9c0b8f6..713d0af 100644 |
265 | --- a/plainbox/impl/launcher.py |
266 | +++ b/plainbox/impl/launcher.py |
267 | @@ -252,5 +252,7 @@ class LauncherDefinition1(LauncherDefinition): |
268 | name='daemon', |
269 | help_text=_('Daemon-specific configuration')) |
270 | |
271 | + manifest = config.Section( |
272 | + help_text=_('Manifest entries to use')) |
273 | |
274 | DefaultLauncherDefinition = LauncherDefinition1 |
275 | diff --git a/plainbox/impl/resource.py b/plainbox/impl/resource.py |
276 | index 26dacc7..a0a74a6 100644 |
277 | --- a/plainbox/impl/resource.py |
278 | +++ b/plainbox/impl/resource.py |
279 | @@ -394,6 +394,7 @@ class ResourceNodeVisitor(ast.NodeVisitor): |
280 | """ |
281 | self._ids_seen_set = set() |
282 | self._ids_seen_list = [] |
283 | + self._manifest_attr_seen_list = [] |
284 | |
285 | @property |
286 | def ids_seen_set(self): |
287 | @@ -409,6 +410,13 @@ class ResourceNodeVisitor(ast.NodeVisitor): |
288 | """ |
289 | return self._ids_seen_list |
290 | |
291 | + @property |
292 | + def manifest_attr_seen_list(self): |
293 | + """ |
294 | + list() of ast.Attribute().attr values seen |
295 | + """ |
296 | + return self._manifest_attr_seen_list |
297 | + |
298 | def visit_Name(self, node): |
299 | """ |
300 | Internal method of NodeVisitor. |
301 | @@ -421,6 +429,20 @@ class ResourceNodeVisitor(ast.NodeVisitor): |
302 | self._ids_seen_set.add(node.id) |
303 | self._ids_seen_list.append(node.id) |
304 | |
305 | + def visit_Attribute(self, node): |
306 | + """ |
307 | + Internal method of NodeVisitor. |
308 | + |
309 | + This method is called whenever generic_visit() looks at an instance of |
310 | + ast.Attribute(). It records the attr identifier |
311 | + """ |
312 | + self._check_node(node) |
313 | + if isinstance(node.value, ast.Name): |
314 | + self.visit_Name(node.value) |
315 | + if node.value.id == 'manifest': |
316 | + if node.attr not in self._manifest_attr_seen_list: |
317 | + self._manifest_attr_seen_list.append(node.attr) |
318 | + |
319 | def visit_Call(self, node): |
320 | """ |
321 | Internal method of NodeVisitor. |
322 | @@ -516,6 +538,7 @@ class ResourceExpression: |
323 | """ |
324 | self._implicit_namespace = implicit_namespace |
325 | self._resource_alias_list = self._analyze(text) |
326 | + self._manifest_id_list = self._analyze_manifest(text) |
327 | self._resource_id_list = [] |
328 | if imports is None: |
329 | imports = () |
330 | @@ -572,6 +595,10 @@ class ResourceExpression: |
331 | ] |
332 | |
333 | @property |
334 | + def manifest_id_list(self): |
335 | + return self._manifest_id_list |
336 | + |
337 | + @property |
338 | def resource_alias_list(self): |
339 | """ |
340 | The alias of the resource object this expression operates on |
341 | @@ -648,6 +675,33 @@ class ResourceExpression: |
342 | else: |
343 | return list(visitor.ids_seen_list) |
344 | |
345 | + def _analyze_manifest(self, text): |
346 | + """ |
347 | + Analyze the expression and return the id of the manifest resource |
348 | + |
349 | + May raise SyntaxError or a ResourceProgramError subclass |
350 | + """ |
351 | + # Use the ast module to build an abstract syntax tree of the expression |
352 | + try: |
353 | + node = ast.parse(text) |
354 | + except SyntaxError: |
355 | + raise ResourceSyntaxError |
356 | + # Use ResourceNodeVisitor to see what kind of ast.Name objects are |
357 | + # referenced by the expression. This may also raise CodeNotAllowed |
358 | + # which should be captured by the higher layers. |
359 | + visitor = ResourceNodeVisitor() |
360 | + visitor.visit(node) |
361 | + # Bail if the expression is not using exactly one resource id |
362 | + if len(visitor.ids_seen_list) == 0: |
363 | + raise NoResourcesReferenced() |
364 | + else: |
365 | + return [ |
366 | + "{}::{}".format(self._implicit_namespace, manifest_id) |
367 | + if "::" not in manifest_id and self._implicit_namespace |
368 | + else manifest_id |
369 | + for manifest_id in list(visitor.manifest_attr_seen_list) |
370 | + ] |
371 | + |
372 | |
373 | def parse_imports_stmt(imports): |
374 | """ |
375 | diff --git a/plainbox/impl/session/assistant.py b/plainbox/impl/session/assistant.py |
376 | index 8ef05e7..653bc4c 100644 |
377 | --- a/plainbox/impl/session/assistant.py |
378 | +++ b/plainbox/impl/session/assistant.py |
379 | @@ -50,9 +50,11 @@ from plainbox.impl.providers.embedded_providers import ( |
380 | from plainbox.impl.result import JobResultBuilder |
381 | from plainbox.impl.result import MemoryJobResult |
382 | from plainbox.impl.runner import JobRunnerUIDelegate |
383 | +from plainbox.impl.secure.config import Unset |
384 | from plainbox.impl.secure.origin import Origin |
385 | from plainbox.impl.secure.qualifiers import select_jobs |
386 | from plainbox.impl.secure.qualifiers import FieldQualifier |
387 | +from plainbox.impl.secure.qualifiers import JobIdQualifier |
388 | from plainbox.impl.secure.qualifiers import PatternMatcher |
389 | from plainbox.impl.secure.qualifiers import RegExpJobQualifier |
390 | from plainbox.impl.session import SessionMetaData |
391 | @@ -209,6 +211,9 @@ class SessionAssistant: |
392 | "configure automatic restart capability") |
393 | allowed_calls[self.use_alternate_restart_strategy] = ( |
394 | "configure automatic restart capability") |
395 | + # Manifest |
396 | + self._manifest_path = os.path.expanduser( |
397 | + '~/.local/share/plainbox/machine-manifest.json') |
398 | |
399 | @raises(UnexpectedMethodCall, LookupError) |
400 | def configure_application_restart( |
401 | @@ -930,7 +935,9 @@ class SessionAssistant: |
402 | desired_job_list = select_jobs( |
403 | self._context.state.job_list, |
404 | [plan.get_qualifier() for plan in self._manager.test_plans] + |
405 | - self._exclude_qualifiers) |
406 | + self._exclude_qualifiers + |
407 | + [JobIdQualifier( |
408 | + 'com.canonical.plainbox::collect-manifest', None, False)]) |
409 | self._context.state.update_desired_job_list(desired_job_list) |
410 | # Set subsequent usage expectations i.e. all of the runtime parts are |
411 | # available now. |
412 | @@ -1244,6 +1251,100 @@ class SessionAssistant: |
413 | if jsm[job.id].result.outcome is None |
414 | ] |
415 | |
416 | + def _strtobool(self, val): |
417 | + return val.lower() in ('y', 'yes', 't', 'true', 'on', '1') |
418 | + |
419 | + @raises(SystemExit, UnexpectedMethodCall) |
420 | + def get_manifest_repr(self) -> 'Dict[List[Dict]]': |
421 | + """ |
422 | + Get the manifest units required by the jobs selection. |
423 | + |
424 | + :returns: |
425 | + A dict of manifest questions. |
426 | + :raises SystemExit: |
427 | + When the launcher manifest section contains invalid entries. |
428 | + :raises UnexpectedMethodCall: |
429 | + If the call is made at an unexpected time. Do not catch this error. |
430 | + It is a bug in your program. The error message will indicate what |
431 | + is the likely cause. |
432 | + """ |
433 | + UsageExpectation.of(self).enforce() |
434 | + # XXX: job_state_map is a bit low level, can we avoid that? |
435 | + jsm = self._context.state.job_state_map |
436 | + todo_list = [ |
437 | + job for job in self._context.state.run_list |
438 | + if jsm[job.id].result.outcome is None |
439 | + ] |
440 | + expression_list = [] |
441 | + manifest_id_set = set() |
442 | + for job in todo_list: |
443 | + if job.get_resource_program(): |
444 | + expression_list.extend( |
445 | + job.get_resource_program().expression_list) |
446 | + for e in expression_list: |
447 | + manifest_id_set.update(e.manifest_id_list) |
448 | + manifest_list = [unit for unit in self._context.unit_list |
449 | + if unit.Meta.name == 'manifest entry' |
450 | + and unit.id in manifest_id_set] |
451 | + manifest_cache = {} |
452 | + if os.path.isfile(self._manifest_path): |
453 | + with open(self._manifest_path, 'rt', encoding='UTF-8') as stream: |
454 | + manifest_cache = json.load(stream) |
455 | + if self._config is not None and self._config.manifest is not Unset: |
456 | + for manifest_id in self._config.manifest: |
457 | + manifest_cache.update( |
458 | + {manifest_id: self._config.manifest[manifest_id]}) |
459 | + manifest_info_dict = dict() |
460 | + for m in manifest_list: |
461 | + prompt = m.prompt() |
462 | + if prompt is None: |
463 | + if m.value_type == 'bool': |
464 | + prompt = "Does this machine have this piece of hardware?" |
465 | + elif m.value_type == 'natural': |
466 | + prompt = "Please enter the requested data:" |
467 | + else: |
468 | + _logger.error("Unsupported value-type: '%s'", m.value_type) |
469 | + continue |
470 | + if prompt not in manifest_info_dict: |
471 | + manifest_info_dict[prompt] = [] |
472 | + manifest_info = { |
473 | + "id": m.id, |
474 | + "partial_id": m.partial_id, |
475 | + "name": m.name, |
476 | + "value_type": m.value_type, |
477 | + } |
478 | + try: |
479 | + value = manifest_cache[m.id] |
480 | + if m.value_type == 'bool': |
481 | + if isinstance(manifest_cache[m.id], str): |
482 | + value = self._strtobool(manifest_cache[m.id]) |
483 | + elif m.value_type == 'natural': |
484 | + value = int(manifest_cache[m.id]) |
485 | + except ValueError: |
486 | + _logger.error( |
487 | + ("Invalid manifest %s value '%s'"), |
488 | + m.id, manifest_cache[m.id]) |
489 | + raise SystemExit(1) |
490 | + except KeyError: |
491 | + value = None |
492 | + manifest_info.update({'value': value}) |
493 | + manifest_info_dict[prompt].append(manifest_info) |
494 | + return manifest_info_dict |
495 | + |
496 | + def save_manifest(self, manifest_answers): |
497 | + """ |
498 | + Record the manifest on disk. |
499 | + """ |
500 | + manifest_cache = dict() |
501 | + if os.path.isfile(self._manifest_path): |
502 | + with open(self._manifest_path, 'rt', encoding='UTF-8') as stream: |
503 | + manifest_cache = json.load(stream) |
504 | + os.makedirs(os.path.dirname(self._manifest_path), exist_ok=True) |
505 | + manifest_cache.update(manifest_answers) |
506 | + print("Saving manifest to {}".format(self._manifest_path)) |
507 | + with open(self._manifest_path, 'wt', encoding='UTF-8') as stream: |
508 | + json.dump(manifest_cache, stream, sort_keys=True, indent=2) |
509 | + |
510 | @raises(ValueError, TypeError, UnexpectedMethodCall) |
511 | def run_job( |
512 | self, job_id: str, ui: 'Union[str, IJobRunnerUI]', |
513 | @@ -1728,6 +1829,8 @@ class SessionAssistant: |
514 | self.remove_all_filters: "to remove all filters", |
515 | self.get_static_todo_list: "to see what is meant to be executed", |
516 | self.get_dynamic_todo_list: "to see what is yet to be executed", |
517 | + self.get_manifest_repr: ( |
518 | + "to get participating manifest units"), |
519 | self.run_job: "to run a given job", |
520 | self.use_alternate_selection: "to change the selection", |
521 | self.hand_pick_jobs: "to generate new selection and use it", |
522 | diff --git a/plainbox/impl/session/remote_assistant.py b/plainbox/impl/session/remote_assistant.py |
523 | index 9b91aeb..0b22c9f 100644 |
524 | --- a/plainbox/impl/session/remote_assistant.py |
525 | +++ b/plainbox/impl/session/remote_assistant.py |
526 | @@ -119,7 +119,7 @@ class BackgroundExecutor(Thread): |
527 | class RemoteSessionAssistant(): |
528 | """Remote execution enabling wrapper for the SessionAssistant""" |
529 | |
530 | - REMOTE_API_VERSION = 8 |
531 | + REMOTE_API_VERSION = 9 |
532 | |
533 | def __init__(self, cmd_callback): |
534 | _logger.debug("__init__()") |
535 | @@ -286,6 +286,12 @@ class RemoteSessionAssistant(): |
536 | job_state.attempts = self._launcher.max_attempts |
537 | return self._sa.get_static_todo_list() |
538 | |
539 | + def get_manifest_repr(self): |
540 | + return self._sa.get_manifest_repr() |
541 | + |
542 | + def save_manifest(self, manifest_answers): |
543 | + return self._sa.save_manifest(manifest_answers) |
544 | + |
545 | def modify_todo_list(self, chosen_jobs): |
546 | self._sa.use_alternate_selection(chosen_jobs) |
547 |
self-approved