Coverage for src/shephex/cli/report.py: 77%
81 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-03-29 18:45 +0100
« prev ^ index » next coverage.py v7.6.1, created at 2025-03-29 18:45 +0100
1from pathlib import Path
2from time import sleep
4import rich_click as click
5from rich import print
6from rich.console import group
7from rich.live import Live
8from rich.table import Table
10from shephex.experiment.context import ExperimentContext
11from shephex.study import Study, StudyRenderer
13from littletable import Table as LittleTable
16class LiveReport:
17 def __init__(self, directory: Path) -> None:
18 self.study = Study(directory, avoid_duplicates=False)
19 self.experiments = self.study.get_experiments(
20 status='all', load_procedure=False
21 )
23 def update_table(self, **kwargs) -> Table:
24 for experiment in self.experiments:
25 try:
26 update_dict = {'identifier': experiment.identifier}
27 # Update from meta file
28 context = ExperimentContext(experiment.shephex_directory)
29 update_dict.update(context.meta)
31 # Update from meta file
32 experiment.meta.load(experiment.shephex_directory)
33 update_dict.update({'status': experiment.meta['status']})
35 self.study.table.update_row_partially(update_dict)
36 except Exception: # pragma: no cover
37 """
38 Ignore exceptions for now.
39 """
40 pass
43class ConditionParser:
45 def __init__(self) -> None:
46 self.types = {'int': int, 'float': float, 'str': str}
48 def comma_seperated(self, value: str, val_type: str) -> list:
49 values = value.split(',')
50 return [self.types[val_type](value) for value in values]
52 def dash_seperated(self, value: str, val_type: str) -> list:
53 start, end = value.split('-')
54 start = self.types[val_type](start)
55 end = self.types[val_type](end)
56 return start, end
58 def parse_conditions(self, renderer: StudyRenderer, filters: list[tuple]) -> None:
59 condition_attrs = [filt[0] for filt in filters]
60 conditions = {attr: [] for attr in condition_attrs}
61 condition_types = {attr: LittleTable.is_in for attr in condition_attrs}
63 for key, value, ftype in filters:
64 if ',' in value: # comma seperated
65 conditions[key].extend(self.comma_seperated(value, ftype))
66 elif '-' in value: # dash seperated
67 start, end = self.dash_seperated(value, ftype)
68 conditions[key] = [start, end]
69 condition_types[key] = LittleTable.within
70 else:
71 conditions[key].append(self.types[ftype](value))
73 for key, values in conditions.items():
74 if condition_types[key] == LittleTable.is_in:
75 conditions[key] = condition_types[key](values)
76 elif condition_types[key] == LittleTable.within:
77 conditions[key] = condition_types[key](*values)
80 renderer.add_condition(**conditions)
82@click.command()
83@click.argument('directories', type=click.Path(exists=True), nargs=-1)
84@click.option('-rr', '--refresh-rate', type=float, default=1)
85@click.option('--total-time', type=float, default=-1)
86@click.option('-l', '--live', is_flag=True, default=True)
87@click.option('-f', '--filters', nargs=3, multiple=True)
88@click.option('-fr', '--filter-range', type=click.Tuple([str, float, float]), multiple=True, nargs=3)
89def report(
90 directories: list[Path],
91 refresh_rate: int,
92 total_time: int,
93 live: bool,
94 filters: tuple,
95 filter_range: tuple[str, float, float]
96) -> None:
97 """
98 Display a live report of the experiments in a directory.
99 """
100 if total_time > 0:
101 iterator = range(int(total_time * refresh_rate))
102 else:
103 iterator = iter(int, 1)
105 reports = {directory: LiveReport(directory) for directory in directories}
106 renderer = StudyRenderer()
108 condition_parser = ConditionParser()
110 for filt in filter_range:
111 converted = (filt[0], f"{filt[1]}-{filt[2]}", 'float')
112 filters += (converted,)
114 condition_parser.parse_conditions(renderer, filters)
116 @group()
117 def get_render_group():
118 for directory, live_report in reports.items():
119 kwargs = {'title': directory}
120 live_report.update_table()
121 yield renderer.get_table(live_report.study, **kwargs)
123 if live:
124 with Live(get_render_group(), refresh_per_second=refresh_rate) as live:
125 for _ in iterator:
126 sleep(1 / refresh_rate)
128 else:
129 print(get_render_group())