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

1from pathlib import Path 

2from time import sleep 

3 

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 

10 

11from shephex.experiment.context import ExperimentContext 

12from shephex.study import Study, StudyRenderer 

13 

14 

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 ) 

21 

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) 

29 

30 # Update from meta file 

31 experiment.meta.load(experiment.shephex_directory) 

32 update_dict.update({'status': experiment.meta['status']}) 

33 

34 self.study.table.update_row_partially(update_dict) 

35 except Exception: # pragma: no cover 

36 """ 

37 Ignore exceptions for now. 

38 """ 

39 pass 

40 

41 

42class ConditionParser: 

43 

44 def __init__(self) -> None: 

45 self.types = {'int': int, 'float': float, 'str': str} 

46 

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] 

50 

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 

56 

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} 

61 

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)) 

71 

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) 

77 

78 

79 renderer.add_condition(**conditions) 

80 

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) 

103 

104 reports = {directory: LiveReport(directory) for directory in directories} 

105 renderer = StudyRenderer() 

106 

107 condition_parser = ConditionParser() 

108 

109 for filt in filter_range: 

110 converted = (filt[0], f"{filt[1]}-{filt[2]}", 'float') 

111 filters += (converted,) 

112 

113 condition_parser.parse_conditions(renderer, filters) 

114 

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) 

121 

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()) 

127 

128 else: 

129 print(get_render_group())