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

1from pathlib import Path 

2from time import sleep 

3 

4import rich_click as click 

5from rich import print 

6from rich.console import group 

7from rich.live import Live 

8from rich.table import Table 

9 

10from shephex.experiment.context import ExperimentContext 

11from shephex.study import Study, StudyRenderer 

12 

13from littletable import Table as LittleTable 

14 

15 

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 ) 

22 

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) 

30 

31 # Update from meta file 

32 experiment.meta.load(experiment.shephex_directory) 

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

34 

35 self.study.table.update_row_partially(update_dict) 

36 except Exception: # pragma: no cover 

37 """ 

38 Ignore exceptions for now. 

39 """ 

40 pass 

41 

42 

43class ConditionParser: 

44 

45 def __init__(self) -> None: 

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

47 

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] 

51 

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 

57 

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} 

62 

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

72 

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) 

78 

79 

80 renderer.add_condition(**conditions) 

81 

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) 

104 

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

106 renderer = StudyRenderer() 

107 

108 condition_parser = ConditionParser() 

109 

110 for filt in filter_range: 

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

112 filters += (converted,) 

113 

114 condition_parser.parse_conditions(renderer, filters) 

115 

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) 

122 

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) 

127 

128 else: 

129 print(get_render_group())