This commit is contained in:
2025-11-17 00:18:45 -03:00
commit 503f4c0fa6
18 changed files with 633 additions and 0 deletions

0
aoc/__init__.py Normal file
View File

71
aoc/cli.py Normal file
View File

@@ -0,0 +1,71 @@
import argparse
import datetime
def get_latest_event_year() -> int:
now = datetime.datetime.now()
year = now.year
if now.month < 12:
year -= 1
return year
def argument_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="aoc")
subcli = parser.add_subparsers(title="command", required=True, dest="command")
solve = subcli.add_parser("solve", help="solve a given day's puzzle")
solve.add_argument(
"-e",
"--example",
action="store_true",
required=False,
help="use example input file instead of real input file",
)
solve.add_argument(
"-y",
"--year",
type=int,
required=False,
help="year of the puzzle (default: current year)",
default=get_latest_event_year(),
)
solve.add_argument("day", type=int, help="day of the puzzle (1-25)")
create = subcli.add_parser(
"create", help="create template and input files for a given day's puzzle"
)
create.add_argument(
"-y",
"--year",
type=int,
required=False,
help="year of the puzzle (default: current year)",
default=get_latest_event_year(),
)
create.add_argument("day", type=int, help="day of the puzzle (1-25)")
download = subcli.add_parser(
"download", help="download input file for a given day's puzzle"
)
download.add_argument(
"-f",
"--force",
action="store_true",
required=False,
help="force re-download even if input file already exists",
)
download.add_argument(
"-y",
"--year",
type=int,
required=False,
help="year of the puzzle (default: current year)",
default=get_latest_event_year(),
)
download.add_argument("day", type=int, help="day of the puzzle (1-25)")
return parser

2
aoc/exceptions.py Normal file
View File

@@ -0,0 +1,2 @@
class AocError(Exception):
pass

113
aoc/input_file.py Normal file
View File

@@ -0,0 +1,113 @@
import requests
import pathlib
from aoc import exceptions
def download(year: int, day: int, session_token: str | None) -> bytes:
url = f"https://adventofcode.com/{year}/day/{day}/input"
cookies = {}
if session_token is not None:
cookies["session"] = session_token
response = requests.get(url, cookies=cookies)
if response.status_code != 200:
raise exceptions.AocError(
f"failed to fetch input file {year}/{day:02} (http:{response.status_code})"
" - is AOC_SESSION_TOKEN environment variable set?"
)
return response.content
def create_input_file(
year: int,
day: int,
session_token: str | None,
path_base: pathlib.Path,
force: bool = False,
exist_ok: bool = False,
):
path = path_base / str(year) / f"{day:02}.txt"
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists() and exist_ok:
return
if path.exists() and not path.stat().st_size == 0 and not force:
raise exceptions.AocError(f"input file {path} already exists")
input_data = download(year, day, session_token)
with open(path, "wb") as f:
f.write(input_data)
def get(
year: int,
day: int,
session_token: str | None,
path_base: pathlib.Path,
) -> str:
path = path_base / str(year) / f"{day:02}.txt"
path.parent.mkdir(parents=True, exist_ok=True)
create_input_file(year, day, session_token, path_base, exist_ok=True)
with open(path) as f:
return f.read()
def _write_file_if_not_exists(
path: pathlib.Path, exist_ok: bool = False, data: bytes | None = None
) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if path.exists() and not exist_ok:
raise exceptions.AocError(f"{path} already exists")
path.touch()
if data is None:
return
with open(path, "wb") as f:
f.write(data)
_CREATE_TEMPLATE: bytes = b"""\
from typing import Any
def part_1(input_data: str) -> Any:
# your part 1 solution here
return None
def part_2(input_data: str) -> Any:
# your part 2 solution here
return None
# alternatively you can implement a "two in one solution" like this
# part_1 and part_2 must be removed or commented out in this case
# if tuple or list is returned elements will be printed on separate lines
# def part_1_2(input_data) -> Any:
# return None
"""
def create(
year: int,
day: int,
solution_base: pathlib.Path,
input_base: pathlib.Path,
example_base: pathlib.Path,
) -> None:
year_path = solution_base / f"year_{year}"
year_path.mkdir(parents=True, exist_ok=True)
_write_file_if_not_exists(year_path / "__init__.py", exist_ok=True)
_write_file_if_not_exists(year_path / f"day_{day:02}.py", data=_CREATE_TEMPLATE)
_write_file_if_not_exists(input_base / str(year) / f"{day:02}.txt")
_write_file_if_not_exists(example_base / str(year) / f"{day:02}.txt")

83
aoc/run.py Normal file
View File

@@ -0,0 +1,83 @@
import importlib.util
import pathlib
from aoc import exceptions
from typing import Any, Callable
import time
_ANSI_ITALIC: str = "\x1b[3m"
_ANSI_BOLD: str = "\x1b[1m"
_ANSI_RESET: str = "\x1b[0m"
def _pprint_ns(ns: int) -> str:
if ns >= 1_000_000_000:
s = ns / 1_000_000_000
return f"{s:.2f}s"
elif ns >= 1_000_000:
ms = ns / 1_000_000
return f"{ms:.2f}ms"
elif ns >= 1_000:
us = ns / 1_000
return f"{us:.2f}µs"
else:
return f"{ns}ns"
def _pprint_result(
year: int, day: int, part: int | str, result: Any, duration_ns: int
) -> None:
part = f"{_ANSI_BOLD}{year}/{day:02}/{part}{_ANSI_RESET}"
if result is None:
solved = f"{_ANSI_ITALIC}(unsolved){_ANSI_RESET}"
print(f"{part}: {solved}")
else:
if isinstance(result, list) or isinstance(result, tuple):
result = "\n".join(str(r) for r in result)
solved = f"{_ANSI_ITALIC}(elapsed: {_pprint_ns(duration_ns)}){_ANSI_RESET}"
print(f"{part}: {solved}\n{result}")
def _run_func(
f: Callable[[str], Any], year: int, day: int, part: int | str, input_data: str
) -> None:
t_1 = time.perf_counter_ns()
result = f(input_data)
t_2 = time.perf_counter_ns()
_pprint_result(year, day, part, result, t_2 - t_1)
def run_day(year: int, day: int, input_data: str, path_base: pathlib.Path) -> None:
try:
module_path = path_base / f"year_{year}" / f"day_{day:02}.py"
spec = importlib.util.spec_from_file_location(
f"year_{year}.day_{day:02}", module_path
)
if spec is None or spec.loader is None:
raise exceptions.AocError()
solution_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(solution_module)
except (ModuleNotFoundError, exceptions.AocError) as e:
raise exceptions.AocError(
f"solution module for {year}/{day:02} not found: run 'python main.py create --year {year} {day}'"
) from e
p_1 = getattr(solution_module, "part_1", None)
p_2 = getattr(solution_module, "part_2", None)
if p_1 is not None and p_2 is not None:
_run_func(p_1, year, day, 1, input_data)
_run_func(p_2, year, day, 2, input_data)
return
p_1_2 = getattr(solution_module, "part_1_2", None)
if p_1_2 is not None:
_run_func(p_1_2, year, day, "1&2", input_data)
return
raise exceptions.AocError(
f"{year}/{day:02} must define part_1 and part_2 or part_1_2"
)