Skip to content

Commit da4ff09

Browse files
committed
scripts improvements
1 parent e4c3630 commit da4ff09

File tree

3 files changed

+131
-41
lines changed

3 files changed

+131
-41
lines changed

scripts/aoc.py

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,45 @@
44

55
import os
66
import subprocess
7+
import sys
78
from dataclasses import dataclass
89
from pathlib import Path
910

1011
try:
1112
import click
1213
except ImportError:
1314
print("This script requires the « click » module.")
14-
exit(1)
15+
16+
if "VIRTUAL_ENV" in os.environ:
17+
print("VirtualEnv detected: try to install from PyPi...")
18+
if os.system(f"{sys.executable} -mpip install click") != 0:
19+
sys.exit(1)
20+
elif sys.platform == "linux":
21+
distro = "unknown"
22+
r = Path("/etc/os-release")
23+
if r.is_file():
24+
for line in r.read_text("utf-8").splitlines():
25+
if line.startswith("ID="):
26+
distro = line[3:].strip().strip('"').strip("'")
27+
break
28+
if distro == "debian" or distro == "ubuntu":
29+
print("Debian/Ubuntu detected: try to install packages...")
30+
if os.system("sudo apt-get install -y python3-click") != 0:
31+
sys.exit(1)
32+
elif distro == "alpine":
33+
print("Alpine detected: try to install packages...")
34+
if os.system("sudo apk add --no-cache py3-click") != 0:
35+
sys.exit(1)
36+
elif distro == "fedora":
37+
print("Fedora detected: try to install packages...")
38+
if os.system("sudo dnf install -y python3-click") != 0:
39+
sys.exit(1)
40+
else:
41+
sys.exit(0)
42+
else:
43+
sys.exit(1)
44+
45+
import click
1546

1647

1748
class AliasedGroup(click.Group):
@@ -61,6 +92,15 @@ def resolve_command(self, ctx, args):
6192
return cmd.name, cmd, args
6293

6394

95+
def get_cli_path():
96+
if os.getuid() == 0:
97+
# probably into a container
98+
cli = Path("/usr/local/bin/aoc")
99+
else:
100+
cli = Path("~/.local/bin/aoc")
101+
return cli
102+
103+
64104
@dataclass
65105
class AocProject:
66106
scripts_dir: Path
@@ -80,15 +120,8 @@ def pass_thru(self, tool: str, args: list, cwd=None):
80120

81121

82122
@click.group(
83-
invoke_without_command=False,
84-
cls=AliasedGroup.aliases(
85-
{
86-
"r": "run",
87-
"p": "private-leaderboard",
88-
"i": "inputs",
89-
"in": "inputs",
90-
}
91-
),
123+
invoke_without_command=True,
124+
cls=AliasedGroup.aliases({"r": "run", "p": "private-leaderboard", "i": "inputs", "in": "inputs"}),
92125
)
93126
@click.pass_context
94127
def aoc(ctx: click.Context):
@@ -103,6 +136,12 @@ def aoc(ctx: click.Context):
103136
if ctx.invoked_subcommand:
104137
return
105138

139+
if not get_cli_path().expanduser().is_file():
140+
ctx.invoke(aoc_install)
141+
else:
142+
click.echo(ctx.get_help())
143+
ctx.exit()
144+
106145

107146
@aoc.command(name="install")
108147
@click.pass_context
@@ -115,20 +154,13 @@ def aoc_install(ctx: click.Context):
115154
if f.is_symlink():
116155
raise click.ClickException("Launch command with the real file, not the symlink.")
117156

118-
if os.getuid() == 0:
119-
# probably into a container
120-
cli = Path("/usr/local/bin/aoc").expanduser()
121-
cli.unlink(True)
122-
cli.parent.mkdir(parents=True, exist_ok=True)
123-
cli.symlink_to(f)
124-
click.echo("Command aoc has been installed in /usr/local/bin .")
125-
126-
else:
127-
cli = Path("~/.local/bin/aoc").expanduser()
128-
cli.unlink(True)
129-
cli.parent.mkdir(parents=True, exist_ok=True)
130-
cli.symlink_to(f)
131-
click.echo("Command aoc has been installed in ~/.local/bin .")
157+
cli_path = get_cli_path()
158+
cli = cli_path.expanduser()
159+
if cli.exists():
160+
cli.unlink()
161+
cli.parent.mkdir(parents=True, exist_ok=True)
162+
cli.symlink_to(f)
163+
click.echo(f"Command aoc has been installed in {cli_path} .")
132164

133165

134166
@aoc.command(name="private-leaderboard")

scripts/lint_python.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ set -eu
44

55
isort -l120 --profile black .
66
black -l120 .
7-
ruff check --ignore E501 . ${*-}
7+
ruff check . ${*-}
88
# flake8 --max-line-length 120

scripts/timings.py

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#!/usr/bin/env python3
22

33
import argparse
4+
import os
45
import sqlite3
6+
import sys
57
import time
68
import typing as t
79
from collections import defaultdict
@@ -10,11 +12,46 @@
1012
from pathlib import Path
1113

1214
try:
15+
import curtsies
1316
import tabulate
14-
from curtsies import Input
1517
except 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

2057
T1 = 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
72110
class 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

78118
class 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

183237
def 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

Comments
 (0)