#!/usr/bin/env pypy3 from scraper import AdventSession from getpass import getpass import importlib import os import re import pathlib import traceback from argparse import ArgumentParser YEAR = 2019 PATH = pathlib.Path(__file__).parent INPUT_PATH = PATH / 'input' class Tests(list): """ Test cases class. A test case is a tuple of the form (puzzle_input, partI_result, partII_result). """ def add(self, test_input, partI=None, partII=None): """Add a test case""" self.append((str(test_input), partI, partII)) def testf(tests, f, part): """ Tests a function f on test cases tests and compares the result to test_case[part]. The test is not run if test_case[part] == None. """ tests = [(t[0], t[part]) for t in tests if t[part] != None] if tests == []: return "No tests" for i, (case, sol) in enumerate(tests): if sol == None: continue r = f(case) if r == sol: continue return "Failed on test {}. Expected {}. Got {}".format(i+1, sol, r) return "OK" class Solver: """ Solver class. A solver is a module with functions partI and partII, an optional preprocessing function preproc, and an optional variable tests of type Tests. """ def __init__(self, mod): """Initilizes the solver instance of module mod.""" def unpack(name, default): """Returns mod.name if it exists and default otherwise.""" return getattr(mod, name) if name in dir(mod) else default dn_pattern = re.compile("\d\d?") self.preproc = unpack('preproc', NotImplemented) self.tests = unpack('tests', []) self.partI = getattr(mod, 'partI') self.partII = getattr(mod, 'partII') def solve(self, puzzle_input): """Solves puzzle input and returns pair of solutions""" puzzle_input = puzzle_input.rstrip() if self.preproc != NotImplemented: puzzle_input = self.preproc(puzzle_input) ptI = self.partI(puzzle_input) ptII = self.partII(puzzle_input) return ptI, ptII def test(self): """Tests the solver and returns pair of test results.""" tests = self.tests if self.preproc != NotImplemented: tests = [(self.preproc(str(t)), s1, s2) for (t, s1, s2) in tests] ptI = testf(tests, self.partI, 1) ptII = testf(tests, self.partII, 2) return ptI, ptII return r parser = ArgumentParser() parser.description = \ """Solves Advent of Code 2019 puzzles. The puzzles are available at https://adventofcode.com/2019/""" parser.add_argument('-d', '--day', action='store', type=str, metavar='DAY[-DAY]', help='solve puzzle of day DAY or of range DAY-DAY') group = parser.add_mutually_exclusive_group() group.add_argument('-i', '--input', action='store', type=str, metavar='INPUT', default=INPUT_PATH, help='use INPUT as puzzle input') group.add_argument('-t', '--test', action='store_true', help="run tests") group.add_argument('--fetch-session', action='store_true', help='fetch puzzle inputs and solutions for session key') def parse_day_arg(s, mind, maxd): """Parses day argument. Returns a list of days.""" split = s.split('-') if len(split) == 1: return [int(split[0])] elif len(split) == 2: d, D = map(int, split) return list(range(max(1, d), min(D+1, 26))) raise ValueError def main(): solver_pattern = re.compile("day(\d\d?).py") matches = map(solver_pattern.fullmatch, os.listdir(PATH)) solvers = {int(s.group(1)) : s.group(0) for s in matches if s != None} mind, maxd = min(solvers), max(solvers) days = sorted(solvers.keys()) args = parser.parse_args() #Handle day argument if args.day != None: try: days = parse_day_arg(args.day, mind, maxd) except ValueError: return 'invalid format of argument -d/--day' #Handle input argument inputs = {} input_path = pathlib.Path('.') num_pattern = re.compile(r'\d\d?') try: fs = os.listdir(args.input) input_path /= args.input for fn in fs: ns = num_pattern.findall(fn) if ns == []: print('warning : ignoring input file {}'.format(fn)) continue n = int(ns[0]) inputs[n] = fn except FileNotFoundError: return "input directory or file doesn't exist" except NotADirectoryError: if len(days) > 1: return "input is file" day = days[0] inputs[day] = args.input #Fetch inputs of session if args.fetch_session: answer = input('Fetch puzzle input and solutions for session key?\n' + 'The session key will not be stored. [Y/n]: ') if 'n' in answer.lower(): return key = getpass('Session key: ') sess = AdventSession(key) anon_pattern = re.compile(r'\(anonymous user #(\d+)\)') user = sess.get_user() print('Fetching inputs for {}'.format(user)) anon_match = anon_pattern.fullmatch(user) if anon_match == None: dir_name = 'user_{}'.format(user.replace(' ', '-')) else: dir_name = 'anon_{}'.format(anon_match.group(1)) if not os.path.exists(dir_name): os.makedirs(dir_name) mssg = 'Choose directory name or press ENTER for [{}]: '.format(dir_name) r = input(mssg) if r != '': dir_name = r path = PATH / pathlib.Path(dir_name) sol_fn = 'solutions' #Create / overwrite solution file open(path / sol_fn, 'a').close() with open(path / sol_fn, 'w') as f: f.write('') input_format = 'day{0:0>2}.txt' for d in days: print('[{}{}] {}/25'.format('#'*d, '.'*(25-d), d), end='\r') try: puzzle_input = sess.get_input(YEAR, d) except e: print('warning : failed to fetch input for day {}'.format(d)) print(e) continue #Create file if it doesn't exist fn = path / input_format.format(d) open(fn, 'a').close() with open(fn, 'w') as f: f.write(puzzle_input) try: solutions = sess.get_solutions(YEAR, d) except e: print('warning : failed to fetch solutions for day {}'.format(d)) print(e) continue with open(path / sol_fn, 'a') as f: f.write(' '.join(solutions) + '\n') print('\nDone') return #Solve days for d in days: print("=" * 6 + " Day {} ".format(d) + "=" * 6) if d not in solvers: return "no solver for day {}".format(d) mod_name = solvers[d][:-3] try: mod = importlib.import_module(mod_name) solver = Solver(mod) except: return "bad solver\n\n{}".format(traceback.format_exc()) try: if args.test: ptI, ptII = solver.test() else: if d not in inputs: return 'no input file' with open(input_path / inputs[d]) as f: puzzle_input = f.read() ptI, ptII = solver.solve(puzzle_input) print("Part One : {}".format(ptI)) print("Part Two : {}".format(ptII)) except: return "error in solver\n\n{}".format(traceback.format_exc()) if __name__ == '__main__': error = main() if error != None: print('error : {}'.format(error))