11#!/usr/bin/env python3
22
33import argparse
4+ import os
45import sqlite3
6+ import sys
57import time
68import typing as t
79from collections import defaultdict
1012from pathlib import Path
1113
1214try :
15+ import curtsies
1316 import tabulate
14- from curtsies import Input
1517except ImportError :
1618 print ("This script requires the « tabulate » and « curtsies » modules." )
17- exit (1 )
19+
20+ if "VIRTUAL_ENV" in os .environ :
21+ print ("VirtualEnv detected: try to install from PyPi..." )
22+ if os .system (f"{ sys .executable } -mpip install tabulate curtsies" ) != 0 :
23+ sys .exit (1 )
24+ elif sys .platform == "linux" :
25+ distro = "unknown"
26+ r = Path ("/etc/os-release" )
27+ if r .is_file ():
28+ for line in r .read_text (encoding = "utf-8" ).splitlines ():
29+ if line .startswith ("ID=" ):
30+ distro = line [3 :].strip ().strip ('"' ).strip ("'" )
31+ break
32+ if distro == "debian" or distro == "ubuntu" :
33+ print ("Debian/Ubuntu detected: try to install packages..." )
34+ if os .system ("sudo apt-get install -y python3-tabulate python3-curtsies" ) != 0 :
35+ sys .exit (1 )
36+ elif distro == "alpine" :
37+ print ("Alpine detected: try to install packages..." )
38+ if os .system ("sudo apk add --no-cache py3-tabulate" ) != 0 :
39+ sys .exit (1 )
40+ elif distro == "fedora" :
41+ print ("Fedora detected: try to install packages..." )
42+ if os .system ("sudo dnf install -y python3-tabulate python3-curtsies" ) != 0 :
43+ sys .exit (1 )
44+ else :
45+ sys .exit (0 )
46+ else :
47+ sys .exit (1 )
48+
49+ import tabulate
50+
51+ try :
52+ import curtsies
53+ except ImportError :
54+ curtsies = None
1855
1956
2057T1 = 0.5
@@ -57,7 +94,8 @@ def aoc_available_puzzles(
5794 return puzzles
5895
5996
60- def fmt_elapsed (elapsed : float , tablefmt ) -> str :
97+ def fmt_elapsed (elapsed : float , _tablefmt ) -> str :
98+ """Format elapsed time with color-coded ANSI escape sequences based on duration thresholds."""
6199 if elapsed < T1 :
62100 return f"\033 [32m{ elapsed :.3f} \033 [0m"
63101 elif elapsed < T2 :
@@ -70,37 +108,52 @@ def fmt_elapsed(elapsed: float, tablefmt) -> str:
70108
71109@dataclass
72110class Stats :
111+ """Container for timing statistics data including headers, table data, and solution timings."""
112+
73113 headers : list
74114 data : dict
75115 solutions : dict
76116
77117
78118class Timings :
119+ """Manages and analyzes execution timing statistics for Advent of Code solutions."""
120+
79121 def __init__ (self , db : sqlite3 .Connection ):
80122 self .year_begin = min (aoc_available_puzzles ())
81123 self .year_end = max (aoc_available_puzzles ())
82124
83125 self .user_inputs = defaultdict (set )
84- for key_input , hash in db .execute ("select key,crc32 from inputs" ):
126+ for key_input , crc32 in db .execute ("select key,crc32 from inputs" ):
85127 year , day , user = key_input .split (":" )
86128 year = int (year )
87129 day = int (day )
88- self .user_inputs [hash ].add (user )
130+ self .user_inputs [crc32 ].add (user )
89131
90132 self .solutions = defaultdict (lambda : defaultdict (dict ))
91133 for key_solution , elapsed , status in db .execute ("select key,elapsed,status from solutions" ):
92134 if status == "ok" :
93- year , day , hash , binary , language = key_solution .split (":" )
135+ year , day , crc32 , _binary , language = key_solution .split (":" )
94136 year = int (year )
95137 day = int (day )
96138 elapsed /= 1_000_000_000
97139
98140 # manage multiple solutions in different dayXX_xxx directories
99141 day_sols = self .solutions [year , day ][language ]
100- other_elapsed = day_sols .get (hash , float ("inf" ))
101- day_sols [hash ] = min (elapsed , other_elapsed )
142+ other_elapsed = day_sols .get (crc32 , float ("inf" ))
143+ day_sols [crc32 ] = min (elapsed , other_elapsed )
102144
103145 def get_stats (self , user : str , lang : str , tablefmt : str ) -> Stats :
146+ """
147+ Compute and return execution timing statistics for a given user and language.
148+
149+ Args:
150+ user: User identifier or aggregation mode ('mean', 'min', 'max', 'minmax')
151+ lang: Programming language identifier
152+ tablefmt: Table format for output formatting
153+
154+ Returns:
155+ Stats object containing headers, data, and solutions timing information.
156+ """
104157 stats = Stats (
105158 headers = ["day" ] + [i for i in range (self .year_begin , self .year_end + 1 )],
106159 data = [[i ] + [None ] * (self .year_end - self .year_begin + 1 ) for i in range (1 , 26 )],
@@ -112,12 +165,12 @@ def get_stats(self, user: str, lang: str, tablefmt: str) -> Stats:
112165 min_elapsed = float ("inf" )
113166 max_elapsed = 0
114167 nb_elapsed = 0
115- for hash , elapsed in languages [lang ].items ():
168+ for key_hash , elapsed in languages [lang ].items ():
116169 if min_elapsed > elapsed :
117170 min_elapsed = elapsed
118171 if max_elapsed < elapsed :
119172 max_elapsed = elapsed
120- if user in ("mean" , "min" , "max" , "minmax" ) or user in self .user_inputs [hash ]:
173+ if user in ("mean" , "min" , "max" , "minmax" ) or user in self .user_inputs [key_hash ]:
121174 total_elapsed += elapsed
122175 nb_elapsed += 1
123176
@@ -145,15 +198,16 @@ def get_stats(self, user: str, lang: str, tablefmt: str) -> Stats:
145198 return stats
146199
147200 def print_stats (self , user : str , lang : str , tablefmt : str = "rounded_outline" ):
201+ """Print timing statistics in a formatted table with performance breakdown."""
148202 stats = self .get_stats (user , lang , tablefmt )
149203
150204 print (tabulate .tabulate (stats .data , stats .headers , tablefmt , floatfmt = ".3f" ))
151205
152- # Don't care of E731... lambda are elegant.
153- timing = lambda a , b : sum (1 for _ in filter (lambda x : a <= x < b , stats .solutions .values ())) # noqa
154- ids = lambda a , b : " " . join (
155- f" { y } : { d :<2 } " for ( y , d ), v in sorted ( stats . solutions . items ()) if a <= v < b
156- ) # noqa
206+ def timing ( a , b ):
207+ return sum (1 for _ in filter (lambda x : a <= x < b , stats .solutions .values ()))
208+
209+ def ids ( a , b ):
210+ return " " . join ( f" { y } : { d :<2 } " for ( y , d ), v in sorted ( stats . solutions . items ()) if a <= v < b )
157211
158212 inf = float ("inf" )
159213 print ()
@@ -181,6 +235,7 @@ def print_stats(self, user: str, lang: str, tablefmt: str = "rounded_outline"):
181235
182236
183237def main ():
238+ """Main entry point for the timings script. Parses command-line arguments and displays timing statistics."""
184239 parser = argparse .ArgumentParser ()
185240 parser .add_argument ("-u" , "--user" , help = "User ID" )
186241 parser .add_argument ("-l" , "--lang" , default = "Rust" , help = "Language" )
@@ -206,6 +261,9 @@ def main():
206261 if not args .browse :
207262 timings .print_stats (args .user , args .lang )
208263
264+ elif curtsies is None :
265+ print ("Install the « curtsies » module." )
266+
209267 else :
210268 sql = "select distinct key from inputs order by key"
211269 users = list (sorted (set (map (lambda row : row [0 ].split (":" )[2 ], db .execute (sql )))))
@@ -246,7 +304,7 @@ def main():
246304 print ()
247305 print ("← → : switch user ↓ ↑ : switch language" )
248306
249- with Input (keynames = "curses" ) as input_generator :
307+ with curtsies . Input (keynames = "curses" ) as input_generator :
250308 for e in input_generator :
251309 if e in ("q" , "Q" , "x" , "X" , "\033 " ):
252310 done = True
0 commit comments