wip
This commit is contained in:
223
.gitignore
vendored
Normal file
223
.gitignore
vendored
Normal file
@@ -0,0 +1,223 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
# Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
# poetry.lock
|
||||
# poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
# pdm.lock
|
||||
# pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
# pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# Redis
|
||||
*.rdb
|
||||
*.aof
|
||||
*.pid
|
||||
|
||||
# RabbitMQ
|
||||
mnesia/
|
||||
rabbitmq/
|
||||
rabbitmq-data/
|
||||
|
||||
# ActiveMQ
|
||||
activemq-data/
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
# .idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
|
||||
# advent of code
|
||||
data/input/*
|
||||
!data/input/.keep
|
||||
|
||||
data/example/*
|
||||
!data/example/.keep
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Matej Janežič
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
0
aoc/__init__.py
Normal file
0
aoc/__init__.py
Normal file
71
aoc/cli.py
Normal file
71
aoc/cli.py
Normal 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
2
aoc/exceptions.py
Normal file
@@ -0,0 +1,2 @@
|
||||
class AocError(Exception):
|
||||
pass
|
||||
113
aoc/input_file.py
Normal file
113
aoc/input_file.py
Normal 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
83
aoc/run.py
Normal 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"
|
||||
)
|
||||
0
data/example/.keep
Normal file
0
data/example/.keep
Normal file
0
data/input/.keep
Normal file
0
data/input/.keep
Normal file
50
main.py
Normal file
50
main.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import pathlib
|
||||
from aoc import cli, input_file, run, exceptions
|
||||
import sys
|
||||
import os
|
||||
import dotenv
|
||||
|
||||
|
||||
SOLUTION_BASE = pathlib.Path("./src/solution")
|
||||
INPUT_BASE = pathlib.Path("./data/input")
|
||||
EXAMPLE_BASE = pathlib.Path("./data/example")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = cli.argument_parser().parse_args()
|
||||
|
||||
session_token = os.getenv("AOC_SESSION_TOKEN")
|
||||
|
||||
try:
|
||||
match args.command:
|
||||
case "solve":
|
||||
input_data = input_file.get(
|
||||
args.year,
|
||||
args.day,
|
||||
session_token,
|
||||
EXAMPLE_BASE if args.example else INPUT_BASE,
|
||||
)
|
||||
run.run_day(args.year, args.day, input_data, SOLUTION_BASE)
|
||||
case "create":
|
||||
input_file.create(
|
||||
args.year,
|
||||
args.day,
|
||||
SOLUTION_BASE,
|
||||
INPUT_BASE,
|
||||
EXAMPLE_BASE,
|
||||
)
|
||||
case "download":
|
||||
input_file.create_input_file(
|
||||
args.year, args.day, session_token, INPUT_BASE, args.force
|
||||
)
|
||||
|
||||
except exceptions.AocError as e:
|
||||
print(f"error: {e}", file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
dotenv.load_dotenv()
|
||||
main()
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/solution/year_2024/__init__.py
Normal file
0
src/solution/year_2024/__init__.py
Normal file
13
src/solution/year_2024/day_01.py
Normal file
13
src/solution/year_2024/day_01.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
def part_1(input_data: str) -> Any:
|
||||
return "part1"
|
||||
|
||||
|
||||
def part_2(input_data: str) -> Any:
|
||||
return "part2"
|
||||
|
||||
|
||||
def part_1_2(input_data: str) -> Any:
|
||||
return "part12"
|
||||
9
src/solution/year_2024/day_02.py
Normal file
9
src/solution/year_2024/day_02.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
def part_1(input_data: str) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
def part_2(input_data: str) -> Any:
|
||||
return "neki"
|
||||
15
src/solution/year_2024/day_03.py
Normal file
15
src/solution/year_2024/day_03.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# autogenerated by 'python main.py create --year {year} {day}'
|
||||
from typing import Any
|
||||
|
||||
|
||||
def part_1(input_data: str) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
def part_2(input_data: str) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
# alternatively your can implement a "two in one solution" like this
|
||||
# def part_1_2(input_data) -> Any:
|
||||
# return None
|
||||
16
src/solution/year_2024/day_07.py
Normal file
16
src/solution/year_2024/day_07.py
Normal file
@@ -0,0 +1,16 @@
|
||||
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 your can implement a "two in one solution" like this
|
||||
# def part_1_2(input_data) -> Any:
|
||||
# return None
|
||||
16
src/solution/year_2024/day_19.py
Normal file
16
src/solution/year_2024/day_19.py
Normal file
@@ -0,0 +1,16 @@
|
||||
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 your can implement a "two in one solution" like this
|
||||
# def part_1_2(input_data) -> Any:
|
||||
# return None
|
||||
Reference in New Issue
Block a user