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