Coverage for seedboxsync/cli/commands/cmd_stats.py: 98%

43 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-26 17:46 +0000

1# 

2# Copyright (C) 2015-2026 Guillaume Kulakowski <guillaume@kulakowski.fr> 

3# 

4# For the full copyright and license information, please view the LICENSE 

5# file that was distributed with this source code. 

6# 

7"""All commands related to statistics operations in SeedboxSync.""" 

8 

9import click 

10from humanize import filesize 

11from peewee import fn 

12from seedboxsync.cli import Context, group, pass_context 

13from seedboxsync.core.dao import Download, typed_peewee_dicts 

14 

15 

16@group( 

17 "stats", 

18 help="Stats operations.", 

19 invoke_without_command=True, 

20 no_args_is_help=False, 

21) # type: ignore[untyped-decorator] 

22@pass_context 

23def cli(ctx: Context) -> None: 

24 """ 

25 Display statistics about completed downloads. 

26 

27 Args: 

28 ctx (Context): The Click context object. 

29 """ 

30 if ctx.invoked_subcommand is None: 

31 click.echo(ctx.get_help()) 

32 click.echo() 

33 ctx.invoke(total) 

34 

35 

36@cli.command("by-month", help="Show statistics aggregated by month.") # type: ignore[untyped-decorator] 

37@pass_context 

38def by_month(ctx: Context) -> None: 

39 """ 

40 Show statistics aggregated by month. 

41 

42 Args: 

43 ctx (Context): The Click context object. 

44 """ 

45 _stats_by_period(ctx, "month", "Month") 

46 

47 

48@cli.command("by-year", help="Show statistics aggregated by year.") # type: ignore[untyped-decorator] 

49@pass_context 

50def by_year(ctx: Context) -> None: 

51 """ 

52 Show statistics aggregated by year. 

53 

54 Args: 

55 ctx (Context): The Click context object. 

56 """ 

57 _stats_by_period(ctx, "year", "Year") 

58 

59 

60@cli.command("total", help="Show total statistics") # type: ignore[untyped-decorator] 

61@pass_context 

62def total(ctx: Context) -> None: 

63 """ 

64 Show total statistics for all completed downloads. 

65 

66 Displays the total number of files and the total size. 

67 

68 Args: 

69 ctx (Context): The Click context object. 

70 """ 

71 query = Download.select().where(Download.finished != 0) 

72 total_files = query.count() 

73 total_size = sum([d.seedbox_size for d in query if d.seedbox_size]) 

74 

75 stats = [ 

76 { 

77 "files": total_files, 

78 "total_size": filesize.naturalsize(total_size, True), 

79 } 

80 ] 

81 

82 click.echo(ctx.render(stats, headers={"files": "Nb files", "total_size": "Total size"})) 

83 

84 

85def _stats_by_period(ctx: Context, period: str, header_label: str) -> None: 

86 """ 

87 Generic helper to calculate statistics by a given period. 

88 

89 Aggregates the number of files and total size for each month or year. 

90 

91 Args: 

92 ctx (Context): The Click context object. 

93 period (str): Either 'month' or 'year' to group statistics. 

94 header_label (str): Label used for rendering the period column. 

95 """ 

96 strftime_format = "%Y-%m" if period == "month" else "%Y" 

97 

98 data = typed_peewee_dicts( 

99 Download.select( 

100 Download.id, 

101 Download.finished, 

102 fn.strftime(strftime_format, Download.finished).alias(period), 

103 Download.seedbox_size, 

104 ) 

105 .where(Download.finished != 0) 

106 .order_by(Download.finished.desc()) 

107 .dicts() 

108 ) 

109 

110 tmp = {} 

111 for download in data: 

112 key = download[period] 

113 size = download["seedbox_size"] 

114 if not key or not size: 

115 continue 

116 if key not in tmp: 

117 tmp[key] = {"files": 0, "total_size": 0.0} 

118 tmp[key]["files"] += 1 

119 tmp[key]["total_size"] += size 

120 

121 stats = [ 

122 { 

123 period: key, 

124 "files": tmp[key]["files"], 

125 "total_size": filesize.naturalsize(tmp[key]["total_size"], True), 

126 } 

127 for key in sorted(tmp) 

128 ] 

129 

130 click.echo( 

131 ctx.render( 

132 stats, 

133 headers={ 

134 period: header_label, 

135 "files": "Nb files", 

136 "total_size": "Total size", 

137 }, 

138 ) 

139 )