aoc2023/aoc/aoc.py
2023-12-12 15:05:20 +01:00

265 lines
7 KiB
Python

from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Generic, Protocol, TypeVar
from urllib.request import Request, urlopen
from html.parser import HTMLParser
from importlib import import_module
from timeit import default_timer
from argparse import ArgumentParser
T1 = TypeVar("T1")
T2 = TypeVar("T2")
class CodeExtractor(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.inpre = False
self.incode = False
self.code: list[str] = []
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag == "code":
self.incode = True
elif tag == "pre":
self.inpre = True
def handle_endtag(self, tag: str) -> None:
if tag == "code":
self.incode = False
elif tag == "pre":
self.inpre = False
def handle_data(self, data: str) -> None:
if self.incode and self.inpre:
self.code.append(data)
class Aoc2(ABC, Generic[T1, T2]):
def __init__(
self, year: int, day: int, example_code_nr1: int = 0, example_code_nr2: int = -1
):
self._year = year
self._day = day
self._example_code_nr1 = example_code_nr1
self._example_code_nr2 = example_code_nr2
with open(".session", encoding="utf8") as f:
self._session = f.read().strip()
@abstractmethod
def part1(self, inpt: T1) -> Any:
...
@abstractmethod
def part2(self, inpt: T2) -> Any:
...
@abstractmethod
def parseinput1(self, inpt: str) -> T1:
...
@abstractmethod
def parseinput2(self, inpt: str) -> T2:
...
def dl_with_cache(self, url: str, cachefilepath: Path) -> str:
cachefilepath.parent.mkdir(parents=True, exist_ok=True)
if cachefilepath.exists():
return cachefilepath.read_text()
request = Request(url, headers={"Cookie": f"session={self._session}"})
with urlopen(request) as response:
inpt: str = response.read().decode()
cachefilepath.write_text(inpt)
return inpt
def get_data_for_day(self) -> str:
return self.dl_with_cache(
f"https://adventofcode.com/{self._year}/day/{self._day}/input",
Path("aoc_cache") / str(self._year) / f"day{self._day}_input",
)
def get_examples_for_day(self) -> list[str]:
site = self.dl_with_cache(
f"https://adventofcode.com/{self._year}/day/{self._day}",
Path("aoc_cache") / str(self._year) / f"day{self._day}_site",
)
code_extractor = CodeExtractor()
code_extractor.feed(site)
return code_extractor.code
def run_example_1(self) -> str:
inpt = self.get_examples_for_day()[self._example_code_nr1]
return str(self.part1(self.parseinput1(inpt)))
def run_example_2(self) -> str:
inpt = self.get_examples_for_day()[self._example_code_nr2]
return str(self.part2(self.parseinput2(inpt)))
def run_1(self) -> str:
inpt = self.get_data_for_day()
return str(self.part1(self.parseinput1(inpt)))
def run_2(self) -> str:
inpt = self.get_data_for_day()
return str(self.part2(self.parseinput2(inpt)))
def run(self) -> None:
print("Example:\t", end="")
print(f"{self.run_example_1()}\t", end="")
print(f"{self.run_example_2()}")
print("Real input:\t", end="")
print(f"{self.run_1()}\t", end="")
print(f"{self.run_2()}")
class Aoc(Generic[T1], Aoc2[T1, T1]):
def parseinput(self, inpt: str) -> T1:
raise NotImplementedError
def parseinput1(self, inpt: str) -> T1:
return self.parseinput(inpt)
def parseinput2(self, inpt: str) -> T1:
return self.parseinput(inpt)
def part2(self, inpt: T1) -> Any:
return self.part1(inpt)
class AocSameParser(Generic[T1]):
@abstractmethod
def parseinput(self, inpt: str) -> T1:
...
def parseinput1(self, inpt: str) -> T1:
return self.parseinput(inpt)
def parseinput2(self, inpt: str) -> T1:
return self.parseinput(inpt)
class AocSameImplementation(Generic[T1]):
@abstractmethod
def part(self, inpt: T1) -> Any:
...
def part1(self, inpt: T1) -> Any:
return self.part(inpt)
def part2(self, inpt: T1) -> Any:
return self.part(inpt)
class AocParseLines(Generic[T1, T2], Protocol):
def parseline1(self, inpt: str) -> T1:
raise NotImplementedError
def parseinput1(self, inpt: str) -> list[T1]:
return [self.parseline1(line) for line in inpt.splitlines()]
def parseline2(self, inpt: str) -> T2:
raise NotImplementedError
def parseinput2(self, inpt: str) -> list[T2]:
return [self.parseline2(line) for line in inpt.splitlines()]
class AocParseLinesSameParser(Generic[T1], AocParseLines[T1, T1]):
def parseline1(self, inpt: str) -> T1:
return self.parseline(inpt)
def parseline2(self, inpt: str) -> T1:
return self.parseline(inpt)
@abstractmethod
def parseline(self, inpt: str) -> T1:
...
def run_latest() -> None:
days = [
import_module(f".{file.stem}", package="aoc")
for file in sorted(Path("aoc").iterdir())
if file.name.endswith(".py") and file.name.startswith("day")
]
day = days[-1]
print(f"Running {day.__name__}")
s_time = default_timer()
day.main()
e_time = default_timer()
print(f"Time: {e_time-s_time}")
def new() -> None:
parser = ArgumentParser()
parser.add_argument("day", type=int)
parser.add_argument("--type1", "-1", default="list[int]")
parser.add_argument("--type2", "-2", default="list[int]")
args = parser.parse_args()
day = args.day
t1 = args.type1
t2 = args.type2
filepath = Path("aoc") / f"day{day:02d}.py"
if filepath.exists():
raise RuntimeError(f"{filepath.name} already exists")
template = f"""from __future__ import annotations
from typing import TypeAlias
from .aoc import Aoc2
P1: TypeAlias = {t1}
P2: TypeAlias = {t2}
class Day{day:02d}(Aoc2[P1, P2]):
def parseinput1(self, inpt: str) -> P1:
raise NotImplementedError()
def parseinput2(self, inpt: str) -> P2:
raise NotImplementedError()
def part1(self, inpt: P1) -> int:
return 0
def part2(self, inpt: P2) -> int:
return 0
def main() -> None:
aoc = Day{day:2d}(2023,{day})
aoc.run()
if __name__ == "__main__":
main()"""
with filepath.open("w") as f:
f.write(template)
def main() -> None:
days = [
import_module(f".{file.stem}", package="aoc")
for file in sorted(Path("aoc").iterdir())
if file.name.endswith(".py") and file.name.startswith("day")
]
b_time = default_timer()
for day in days:
print(f"Running {day.__name__}")
s_time = default_timer()
day.main()
e_time = default_timer()
print(f"Time: {e_time-s_time}")
print()
f_time = default_timer()
print(f"Overall time: {f_time - b_time}")
if __name__ == "__main__":
main()