22
33import argparse
44import itertools
5- import json
65import os
76import subprocess
87import time
1413from pathlib import Path
1514from zlib import crc32
1615import sqlite3
16+ import shutil
1717
1818RED = "\033 [91m"
1919GREEN = "\033 [92m"
3333
3434LANGUAGES = {
3535 "Python" : "{year}/day{day}/day{day}.py" ,
36- "PyPy" : "{year}/day{day}/day{day}.py" ,
3736 "Rust" : "{year}/target/release/day{day}" ,
3837 "C" : "{year}/build/day{day}_c" ,
3938 "C++" : "{year}/build/day{day}_cpp" ,
4039}
4140
42- DEFAULT_LANGUAGES = {"Python" , "Rust" }
43-
4441INTERPRETERS = {
45- "Python" : "python3" ,
46- "PyPy" : "pypy3" ,
42+ "Python" : {
43+ "Python" : "python3" ,
44+ "PyPy" : ".pypy3.10/bin/python" ,
45+ "Py3.10" : ".py3.10/bin/python" ,
46+ "Py3.11" : ".py3.11/bin/python" ,
47+ "Py3.12" : ".py3.12/bin/python" ,
48+ "Py3.13" : ".py3.13/bin/python" ,
49+ }
4750}
4851
4952
@@ -61,7 +64,13 @@ def get_cache():
6164 " mtime_ns int,"
6265 " elapsed float,"
6366 " status text,"
64- " answers text);"
67+ " answers text"
68+ ");"
69+ "create table if not exists inputs ("
70+ " key text primary key not null,"
71+ " mtime_ns int,"
72+ " crc32 text"
73+ ");"
6574 )
6675 cache = {"db" : cache_db , "modified" : False }
6776 globals ()["_cache" ] = cache
@@ -78,20 +87,18 @@ def save_cache():
7887 # print(f"{FEINT}{ITALIC}cache commited{RESET}")
7988
8089
81- def check_cache (key , file_timestamp : Path , no_age_check = False ):
90+ def check_cache (key , file_timestamp : Path , table : str , columns : t . Iterable [ str ], no_age_check = False ):
8291 cache = get_cache ()
8392 key = str (key )
8493 db = cache ["db" ]
85- cursor = db .execute ("select mtime_ns,elapsed,status,answers from solutions where key=?" , (key ,))
94+ db .row_factory = sqlite3 .Row
95+ cursor = db .execute (f"select * from `{ table } ` where key=?" , (key ,))
8696 row = cursor .fetchone ()
8797 if row :
8898 timestamp = file_timestamp .stat ().st_mtime_ns
89- if row [0 ] == timestamp or no_age_check :
90- return {
91- "elapsed" : row [1 ],
92- "status" : row [2 ],
93- "answers" : row [3 ].split ("\n " ),
94- }
99+ if row ["mtime_ns" ] == timestamp or no_age_check :
100+ return dict ((column , row [column ]) for column in columns )
101+
95102 else :
96103 # seconds = round((timestamp - e["timestamp"]) / 1000000000)
97104 # delta = timedelta(seconds=seconds)
@@ -103,34 +110,40 @@ def check_cache(key, file_timestamp: Path, no_age_check=False):
103110 print (f"{ FEINT } { ITALIC } missing cache for { key } { RESET } " , end = f"{ CLEAR_EOL } { CR } " )
104111
105112
106- def update_cache (key , timestamp : Path , elapsed : float , status : str , answers : t . Iterable ) :
113+ def update_cache (key , timestamp : Path , table : str , row : t . Dict [ str , t . Union [ str , int ]]) -> None :
107114 cache = get_cache ()
108115 db = cache ["db" ]
109- key = str (key )
110116
111- db .execute (
112- "insert or replace into solutions (key,mtime_ns,elapsed,status,answers) values (?,?,?,?,?)" ,
113- (key , timestamp .stat ().st_mtime_ns , elapsed , status , "\n " .join (answers )),
114- )
117+ sql = f"insert or replace into `{ table } ` (key,mtime_ns"
118+ values = [str (key ), timestamp .stat ().st_mtime_ns ]
115119
116- # cache["modified"] = True
117- db .commit ()
120+ for k , v in row .items ():
121+ sql += f",`{ k } `"
122+ values .append (v )
118123
119- return {
120- "elapsed" : elapsed ,
121- "status" : status ,
122- "answers" : answers ,
123- }
124+ sql += ") values (?,?"
125+ sql += ",?" * len (row )
126+ sql += ")"
127+
128+ db .execute (sql , values )
129+
130+ db .commit ()
124131
125132
126- def run (prog : Path , lang : str , file : Path , solution : t .List , warmup : bool ) -> t .Dict [str , t .Any ]:
133+ def run (
134+ prog : Path ,
135+ lang : str ,
136+ interpreter : t .Union [None , str ],
137+ file : Path ,
138+ solution : t .List ,
139+ warmup : bool ,
140+ ) -> t .Dict [str , t .Any ]:
127141 if not prog .is_file ():
128142 return
129143
130144 cmd = [prog .absolute ().as_posix ()]
131145
132146 # add the interpreter
133- interpreter = INTERPRETERS .get (lang )
134147 if interpreter :
135148 cmd .insert (0 , interpreter )
136149
@@ -156,7 +169,7 @@ def run(prog: Path, lang: str, file: Path, solution: t.List, warmup: bool) -> t.
156169 else :
157170 status = "unknown"
158171
159- return {"elapsed" : elapsed , "status" : status , "answers" : answers . split ( " \n " ) }
172+ return {"elapsed" : elapsed , "status" : status , "answers" : answers }
160173
161174
162175def make (year : Path , source : Path , dest : Path , cmd : str ):
@@ -211,7 +224,9 @@ def load_data(filter_year, filter_user):
211224
212225 assert len (f .parts ) == 4
213226
214- if filter_user and f .parent .parent .name != filter_user :
227+ user = f .parent .parent .name
228+
229+ if filter_user and user != filter_user :
215230 continue
216231
217232 year = int (f .parent .name )
@@ -220,12 +235,14 @@ def load_data(filter_year, filter_user):
220235 if filter_year != 0 and year != filter_year :
221236 continue
222237
223- e = check_cache (f , f )
238+ key = f"{ year } :{ day } :{ user } "
239+
240+ e = check_cache (key , f , "inputs" , ("crc32" ,))
224241 if e :
225- crc = e ["status " ]
242+ crc = e ["crc32 " ]
226243 else :
227244 crc = hex (crc32 (f .read_bytes ().strip ()) & 0xFFFFFFFF )
228- update_cache (f , f , 0 , crc , [] )
245+ update_cache (key , f , "inputs" , { "crc32" : crc } )
229246
230247 if crc not in inputs [year , day ]:
231248 inputs [year , day ][crc ] = f
@@ -239,7 +256,15 @@ def load_data(filter_year, filter_user):
239256
240257
241258def run_day (
242- year : int , day : int , mday : str , day_inputs : t .Dict , day_sols : t .Dict , problems : t .Set , filter_lang , refresh , dry_run
259+ year : int ,
260+ day : int ,
261+ mday : str ,
262+ day_inputs : t .Dict ,
263+ day_sols : t .Dict ,
264+ languages : dict ,
265+ problems : t .Set ,
266+ refresh : bool ,
267+ dry_run : bool ,
243268):
244269 elapsed = defaultdict (list )
245270
@@ -259,14 +284,7 @@ def run_day(
259284
260285 results = set ()
261286
262- for lang , pattern in LANGUAGES .items ():
263- if filter_lang == "all" :
264- pass
265- elif filter_lang :
266- if lang .lower () != filter_lang .lower ():
267- continue
268- elif lang not in DEFAULT_LANGUAGES :
269- continue
287+ for lang , (pattern , interpreter ) in languages .items ():
270288
271289 prog = Path (pattern .format (year = year , day = mday ))
272290 key = ":" .join (map (str , (year , day , crc , prog , lang .lower ())))
@@ -275,16 +293,16 @@ def run_day(
275293 e = None
276294 in_cache = False
277295 else :
278- e = check_cache (key , prog , dry_run )
296+ e = check_cache (key , prog , "solutions" , ( "elapsed" , "status" , "answers" ), dry_run )
279297 in_cache = e is not None
280298
281299 if not in_cache and not dry_run :
282300
283- e = run (prog , lang , file , day_sols .get (crc ), warmup [lang ])
301+ e = run (prog , lang , interpreter , file , day_sols .get (crc ), warmup [lang ])
284302
285303 if e :
286304 warmup [lang ] = False
287- e = update_cache (key , prog , e [ "elapsed" ] , e [ "status" ], e [ "answers" ] )
305+ update_cache (key , prog , "solutions" , e )
288306
289307 if not e :
290308 continue
@@ -296,14 +314,16 @@ def run_day(
296314
297315 status_color = {"missing" : MAGENTA , "unknown" : GRAY , "error" : RED , "ok" : GREEN }[e ["status" ]]
298316
317+ answers = e ["answers" ].split ("\n " )
318+
299319 line = (
300320 f"{ CR } { RESET } { CLEAR_EOL } "
301321 f"{ prefix } "
302322 f" { YELLOW } { lang :<7} { RESET } :"
303323 f" { status_color } { e ['status' ]:7} { RESET } "
304324 f" { WHITE } { e ['elapsed' ]/ 1e9 :7.3f} s"
305325 f" { GRAY } { '☽' if in_cache else ' ' } "
306- f" { status_color } { str (e [ ' answers' ] ):<40} { RESET } "
326+ f" { status_color } { str (answers ):<40} { RESET } "
307327 f"{ info } "
308328 )
309329 print (line )
@@ -314,7 +334,7 @@ def run_day(
314334 if not in_cache and e ["elapsed" ] / 1e9 > 5 :
315335 save_cache ()
316336
317- results .add (" " .join (e [ " answers" ] ))
337+ results .add (" " .join (answers ))
318338
319339 elapsed [lang ].append (e ["elapsed" ] / 1e9 )
320340
@@ -330,9 +350,83 @@ def run_day(
330350 return dict ((lang , sum (t ) / len (t )) for lang , t in elapsed .items ()), nb_samples
331351
332352
353+ def get_languages (filter_lang : t .Iterable [str ]) -> t .Dict [str , t .Tuple [str , t .Union [str , None ]]]:
354+
355+ filter_lang = set (map (str .casefold , filter_lang or ()))
356+
357+ languages = {}
358+ for lang , v in LANGUAGES .items ():
359+
360+ if lang in INTERPRETERS :
361+ for lang2 , interpreter in INTERPRETERS [lang ].items ():
362+
363+ if filter_lang and lang2 .casefold () not in filter_lang :
364+ continue
365+
366+ if "/" not in interpreter and "\\ " not in interpreter :
367+ interpreter = shutil .which (interpreter )
368+ languages [lang2 ] = (v , interpreter )
369+ else :
370+ interpreter = Path (interpreter ).expanduser ().absolute ()
371+ if interpreter .is_file () and (interpreter .stat ().st_mode & os .X_OK ) != 0 :
372+ languages [lang2 ] = (v , interpreter .as_posix ())
373+ else :
374+ # print(f"language {lang2} : interpreter {interpreter} not found")
375+ pass
376+ else :
377+
378+ if filter_lang and lang .casefold () not in filter_lang :
379+ continue
380+ languages [lang ] = (v , None )
381+
382+ return languages
383+
384+
385+ def install_venv (interpreter : Path ):
386+
387+ try :
388+ slug = 'import sys;print(((hasattr(sys, "subversion") and getattr(sys, "subversion")) or ("Py",))[0] + f"{sys.version_info.major}.{sys.version_info.minor}")'
389+
390+ slug = subprocess .check_output ([interpreter , "-c" , slug ]).decode ().strip ()
391+
392+ venv = "." + slug .lower ()
393+
394+ # subprocess.check_call([interpreter, "-mensurepip"])
395+ subprocess .check_output ([interpreter , "-mvenv" , venv ])
396+ subprocess .check_call (
397+ [
398+ f"{ venv } /bin/python3" ,
399+ "-mpip" ,
400+ "install" ,
401+ "--no-input" ,
402+ "--quiet" ,
403+ "--upgrade" ,
404+ "pip" ,
405+ ]
406+ )
407+ subprocess .check_call (
408+ [
409+ f"{ venv } /bin/python3" ,
410+ "-mpip" ,
411+ "install" ,
412+ "--no-input" ,
413+ "--quiet" ,
414+ "--upgrade" ,
415+ "-r" ,
416+ "scripts/requirements.txt" ,
417+ ]
418+ )
419+
420+ print (f"Virtual environment for { MAGENTA } { interpreter } { RESET } installed into: { GREEN } { venv } { RESET } " )
421+
422+ except subprocess .CalledProcessError as e :
423+ print (e )
424+
425+
333426def main ():
334427 parser = argparse .ArgumentParser ()
335- parser .add_argument ("-l" , "--language" , type = str , metavar = "LANG" , help = "filter by language" )
428+ parser .add_argument ("--venv" , type = Path , help = "install virtual environment" )
429+ parser .add_argument ("-l" , "--language" , type = str , action = "append" , metavar = "LANG" , help = "filter by language" )
336430 parser .add_argument ("-r" , "--refresh" , action = "store_true" , help = "relaunch solutions" )
337431 parser .add_argument ("-n" , "--dry-run" , action = "store_true" , help = "do not run" )
338432 parser .add_argument ("--no-build" , action = "store_true" , help = "do not build" )
@@ -341,21 +435,24 @@ def main():
341435
342436 args = parser .parse_args ()
343437
344- filter_year = 0 if len (args .n ) == 0 else int (args .n .pop (0 ))
345- filter_day = set (args .n )
346- if args .language == "cpp" :
347- args .language = "c++"
348-
349438 try :
350- os .chdir (Path (__file__ ).parent .parent )
351-
352439 problems = []
353440 stats_elapsed = dict ()
354441
442+ os .chdir (Path (__file__ ).parent .parent )
443+
444+ if args .venv :
445+ return install_venv (args .venv )
446+
447+ filter_year = 0 if len (args .n ) == 0 else int (args .n .pop (0 ))
448+ filter_day = set (args .n )
449+
355450 if not args .no_build :
356451 build_all (filter_year )
357452 print (end = f"{ CR } { CLEAR_EOL } " )
358453
454+ languages = get_languages (args .language )
455+
359456 inputs , sols = load_data (filter_year , args .filter_user )
360457
361458 for year in range (2015 , 2024 ):
@@ -375,8 +472,8 @@ def main():
375472 mday ,
376473 inputs [year , day ],
377474 sols [year , day ],
475+ languages ,
378476 problems ,
379- args .language ,
380477 args .refresh ,
381478 args .dry_run ,
382479 )
0 commit comments