Skip to content

Commit 1486757

Browse files
committed
CLI wrapper
1 parent b2ae635 commit 1486757

File tree

1 file changed

+227
-0
lines changed

1 file changed

+227
-0
lines changed

scripts/aoc.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
#!/usr/bin/env python3
2+
3+
# I finally wrote the CLI as a wrapper of my own scripts and [aoc-cli](https://github.com/scarvalhojr/aoc-cli) 😎
4+
5+
import os
6+
import subprocess
7+
import typing as t
8+
from dataclasses import dataclass
9+
from pathlib import Path
10+
11+
import click
12+
13+
14+
class AliasedGroup(click.Group):
15+
"""This subclass of a group supports looking up aliases in a config
16+
file and with a bit of magic.
17+
"""
18+
19+
_aliases = {}
20+
21+
@classmethod
22+
def aliases(cls, a):
23+
class AliasedClass(cls):
24+
_aliases = a
25+
26+
return AliasedClass
27+
28+
def get_command(self, ctx, cmd_name):
29+
30+
# Step one: bulitin commands as normal
31+
rv = click.Group.get_command(self, ctx, cmd_name)
32+
if rv is not None:
33+
return rv
34+
35+
# Step two: find the config object and ensure it's there. This
36+
# will create the config object is missing.
37+
# cfg = ctx.ensure_object(self.__class__)
38+
cfg = self
39+
40+
# Step three: look up an explicit command alias in the config
41+
if cmd_name in cfg._aliases:
42+
actual_cmd = cfg._aliases[cmd_name]
43+
return click.Group.get_command(self, ctx, actual_cmd)
44+
45+
# Alternative option: if we did not find an explicit alias we
46+
# allow automatic abbreviation of the command. "status" for
47+
# instance will match "st". We only allow that however if
48+
# there is only one command.
49+
matches = [x for x in self.list_commands(ctx) if x.lower().startswith(cmd_name.lower())]
50+
if not matches:
51+
return None
52+
elif len(matches) == 1:
53+
return click.Group.get_command(self, ctx, matches[0])
54+
ctx.fail(f"Too many matches: {', '.join(sorted(matches))}")
55+
56+
def resolve_command(self, ctx, args):
57+
# always return the command's name, not the alias
58+
_, cmd, args = super().resolve_command(ctx, args)
59+
return cmd.name, cmd, args
60+
61+
62+
@dataclass
63+
class AocProject:
64+
scripts_dir: Path
65+
aoc_root: Path
66+
67+
def pass_thru(self, tool: str, args: list, cwd=None):
68+
69+
if not Path(os.getcwd()).is_relative_to(self.aoc_root):
70+
raise click.ClickException("not in AoC project")
71+
72+
cmd = [self.scripts_dir / tool]
73+
cmd.extend(args)
74+
subprocess.call(cmd, cwd=cwd)
75+
76+
77+
@click.group(
78+
invoke_without_command=False,
79+
cls=AliasedGroup.aliases(
80+
{
81+
"r": "run",
82+
"p": "private-leaderboard",
83+
}
84+
),
85+
)
86+
@click.pass_context
87+
def aoc(ctx: click.Context):
88+
"""CLI for Advent of Code daily tasks."""
89+
90+
script = Path(__file__).resolve()
91+
assert script.name == "aoc.py"
92+
assert script.parent.name == "scripts"
93+
94+
ctx.obj = AocProject(script.parent, script.parent.parent)
95+
96+
if ctx.invoked_subcommand:
97+
return
98+
99+
100+
@aoc.command(name="install")
101+
@click.pass_context
102+
def aoc_install(ctx: click.Context):
103+
"""
104+
Install the CLI into ~/.local/bin .
105+
"""
106+
107+
f = Path(__file__)
108+
if f.is_symlink():
109+
raise click.ClickException("Launch command with the real file, not the symlink.")
110+
111+
cli = Path("~/.local/bin/aoc").expanduser()
112+
cli.unlink(True)
113+
cli.symlink_to(f)
114+
115+
click.echo("Command aoc has been installed in ~/.local/bin .")
116+
117+
118+
@aoc.command(name="private-leaderboard")
119+
@click.argument("id", type=int)
120+
@click.pass_context
121+
def aoc_private_leaderboard(ctx: click.Context, id: int):
122+
"""
123+
Show the state of a private leaderboard.
124+
"""
125+
subprocess.run(["aoc-cli", "private-leaderboard", str(id)])
126+
127+
128+
@aoc.command(name="calendar", context_settings=dict(ignore_unknown_options=True, allow_extra_args=True))
129+
@click.pass_context
130+
def aoc_calendar(ctx: click.Context):
131+
"""
132+
Show Advent of Code calendar and stars collected.
133+
"""
134+
subprocess.run(["aoc-cli", "calendar"] + ctx.args)
135+
136+
137+
@aoc.command(name="download", context_settings=dict(ignore_unknown_options=True, allow_extra_args=True))
138+
@click.pass_context
139+
def aoc_download(ctx: click.Context):
140+
"""
141+
Save puzzle description and input to files.
142+
"""
143+
subprocess.run(["aoc-cli", "download"] + ctx.args)
144+
145+
146+
@aoc.command(name="puzzle")
147+
@click.argument("day", type=int)
148+
@click.pass_context
149+
def aoc_puzzle(ctx: click.Context, day: int):
150+
"""
151+
Get input and write templates.
152+
"""
153+
ctx.obj.pass_thru("puzzle.sh", [str(day)])
154+
155+
156+
@aoc.command(name="run", context_settings=dict(ignore_unknown_options=True, allow_extra_args=True))
157+
@click.pass_context
158+
def aoc_run(ctx: click.Context):
159+
"""
160+
Run all puzzles.
161+
"""
162+
ctx.obj.pass_thru("runall.py", ctx.args)
163+
164+
165+
@aoc.command(name="clippy")
166+
@click.pass_context
167+
def aoc_clippy(ctx: click.Context):
168+
"""
169+
Run the Rust clippy checker.
170+
"""
171+
cwd = Path(os.getcwd())
172+
173+
if cwd == ctx.obj.aoc_root:
174+
for year in sorted(cwd.glob("*")):
175+
if year.name.isdigit() and int(year.name) >= 2015:
176+
print(f"Year {year.name}")
177+
ctx.obj.pass_thru("lint_rust.sh", [], cwd=year)
178+
else:
179+
180+
if not (cwd / "Cargo.toml").is_file():
181+
raise click.ClickException("need a Cargo.toml file")
182+
ctx.obj.pass_thru("lint_rust.sh", [])
183+
184+
185+
@aoc.command(name="answers", context_settings=dict(ignore_unknown_options=True, allow_extra_args=True))
186+
@click.pass_context
187+
def aoc_answers(ctx: click.Context):
188+
"""
189+
Submits answer or the them.
190+
"""
191+
ctx.obj.pass_thru("answers.py", ctx.args)
192+
193+
194+
@aoc.command(name="readme")
195+
@click.pass_context
196+
def aoc_readme(ctx: click.Context):
197+
"""
198+
Make all the README.md.
199+
"""
200+
ctx.obj.pass_thru("answers.py", ["--readme", "-w"])
201+
202+
203+
@aoc.command(name="inputs")
204+
@click.pass_context
205+
def aoc_inputs(ctx: click.Context):
206+
"""
207+
Show the number of available inputs.
208+
"""
209+
ctx.obj.pass_thru("inputs.py", [])
210+
211+
212+
@aoc.command(name="scores")
213+
@click.option("-y", "--year", type=int, help="Year")
214+
@click.argument("id", type=int)
215+
@click.pass_context
216+
def aoc_scores(ctx: click.Context, year, id: int):
217+
"""
218+
Show a private leaderboard.
219+
"""
220+
args = [str(id)]
221+
if year:
222+
args.extend(["--year", str(year)])
223+
ctx.obj.pass_thru("score.py", args)
224+
225+
226+
if __name__ == "__main__":
227+
aoc()

0 commit comments

Comments
 (0)