From 1563d012a0c4058d4b240fb5ab37bdcdf0e1ee06 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 22:42:24 +0200 Subject: [PATCH 001/176] Move input getter to new solution runner directory --- input_getter.py => solution_runner/input_getter.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename input_getter.py => solution_runner/input_getter.py (100%) diff --git a/input_getter.py b/solution_runner/input_getter.py similarity index 100% rename from input_getter.py rename to solution_runner/input_getter.py From 51ccb61584317ddff13a51db42bb577c33d4ae4a Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 22:45:17 +0200 Subject: [PATCH 002/176] Create Python packages requirements file --- requirements.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e69de29b From 3484058b8c5e7002d35b1100e9d8a8ac9fd44e5e Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 22:46:03 +0200 Subject: [PATCH 003/176] Add `requests` dependency to packages requirements file --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index e69de29b..663bd1f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file From b892fb7bd061395aea14da520b0ffc6f85ebfc71 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 22:46:34 +0200 Subject: [PATCH 004/176] Add `click` dependency to packages requirements file --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 663bd1f6..23adf640 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -requests \ No newline at end of file +requests +click \ No newline at end of file From a60498dfa4d4fb5c503b8f2d66ceaa882e4f055a Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 23:05:29 +0200 Subject: [PATCH 005/176] Create file holding CLI --- solution_runner/cli.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 solution_runner/cli.py diff --git a/solution_runner/cli.py b/solution_runner/cli.py new file mode 100644 index 00000000..e69de29b From 1d62c60e0e6e02caf56d0cfc2993bef36d3897ca Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 23:07:29 +0200 Subject: [PATCH 006/176] Create constants file --- solution_runner/consts.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 solution_runner/consts.py diff --git a/solution_runner/consts.py b/solution_runner/consts.py new file mode 100644 index 00000000..e69de29b From f3101e48feda136b489e9778b1ead3e91c8b8eeb Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 23:08:06 +0200 Subject: [PATCH 007/176] Create CLI constants class --- solution_runner/consts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solution_runner/consts.py b/solution_runner/consts.py index e69de29b..b56d2b20 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -0,0 +1,2 @@ +class CliConstants: + ... From fcce4ed73984d1c5bc89cf77d75a30b528578654 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 23:09:00 +0200 Subject: [PATCH 008/176] Add CLI context constants --- solution_runner/consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/consts.py b/solution_runner/consts.py index b56d2b20..11a7fb31 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,2 +1,2 @@ class CliConstants: - ... + CONTEXT = {'help_option_names': ['-h', '--help']} From d82c13ecbdeece4285f8759229ad244edc8e4c82 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 23:16:14 +0200 Subject: [PATCH 009/176] Create CLI --- solution_runner/cli.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index e69de29b..291710c9 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -0,0 +1,16 @@ +import click + +from consts import CliConstants + + +@click.group(context_settings=CliConstants.CONTEXT) +def cli(): + """ + Main CLI holding all Advent of Code related commands. + + For more information about Advent of Code, see https://adventofcode.com. + """ + + +if __name__ == '__main__': + cli() From 206d74cec7d22cd8b777e2f93d5215f04446e519 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 23:26:05 +0200 Subject: [PATCH 010/176] Add file holding defaults and choices functions --- solution_runner/defaults_and_choices.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 solution_runner/defaults_and_choices.py diff --git a/solution_runner/defaults_and_choices.py b/solution_runner/defaults_and_choices.py new file mode 100644 index 00000000..e69de29b From 68280f0ea08f3ed65115ad8fd461d00c93dd7e1a Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 23:28:13 +0200 Subject: [PATCH 011/176] Move default year function to new file --- solution_runner/defaults_and_choices.py | 13 +++++++++++++ solution_runner/input_getter.py | 10 ---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/solution_runner/defaults_and_choices.py b/solution_runner/defaults_and_choices.py index e69de29b..4ac88de7 100644 --- a/solution_runner/defaults_and_choices.py +++ b/solution_runner/defaults_and_choices.py @@ -0,0 +1,13 @@ +from datetime import datetime + + +def get_default_year() -> int: + """ + :return: default year which is the current year if it's December, last year otherwise + """ + today = datetime.today() + current_year = today.year + if today.month == 12: + return current_year + else: + return current_year - 1 diff --git a/solution_runner/input_getter.py b/solution_runner/input_getter.py index 06db8b28..b389c0d4 100644 --- a/solution_runner/input_getter.py +++ b/solution_runner/input_getter.py @@ -1,4 +1,3 @@ -from datetime import datetime from pathlib import Path import requests @@ -23,15 +22,6 @@ def get_input(year: str, day_number: str) -> str: return request.text -def _get_default_year() -> int: - today = datetime.today() - current_year = today.year - if today.month == 12: - return current_year - else: # If it not December, chances are we are solving last year's challenges. - return current_year - 1 - - def get_year_input() -> str: current_year = _get_default_year() year_input = input(f'Enter year ({current_year} is default): ') From dc56b0196755eefe4ea5097dc0119ccde1e9bb36 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 3 Dec 2021 23:30:30 +0200 Subject: [PATCH 012/176] Create setup command --- solution_runner/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index 291710c9..9672003b 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -12,5 +12,10 @@ def cli(): """ +@cli.command() +def setup(): + """Set up a solution: fetch input and create solution files.""" + + if __name__ == '__main__': cli() From b43f2b49ca8a4d3ddf9a9e24d26ff5309b1288f0 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 00:11:42 +0200 Subject: [PATCH 013/176] Add year option for setup command --- solution_runner/cli.py | 6 +++++- solution_runner/consts.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index 9672003b..d7358c6b 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -1,6 +1,8 @@ import click +import consts from consts import CliConstants +from defaults_and_choices import get_default_year @click.group(context_settings=CliConstants.CONTEXT) @@ -13,7 +15,9 @@ def cli(): @cli.command() -def setup(): +@click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, get_default_year()), + default=get_default_year(), help='year of challenge setting up solution for') +def setup(year: int): """Set up a solution: fetch input and create solution files.""" diff --git a/solution_runner/consts.py b/solution_runner/consts.py index 11a7fb31..10010d6e 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,2 +1,5 @@ +FIRST_AOC_YEAR = 2015 + + class CliConstants: CONTEXT = {'help_option_names': ['-h', '--help']} From 80c1cb18cdf3ae8233c7ae1028890e9e7b4b15d4 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 00:25:32 +0200 Subject: [PATCH 014/176] Add day option for setup command --- solution_runner/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index d7358c6b..b8ceb92f 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -17,7 +17,8 @@ def cli(): @cli.command() @click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, get_default_year()), default=get_default_year(), help='year of challenge setting up solution for') -def setup(year: int): +@click.option('-d', '--day', type=click.IntRange(1, 25), help='day of challenge setting up solution for') +def setup(year: int, day: int): """Set up a solution: fetch input and create solution files.""" From 3a21fa7a9e640613838424bb80823c1cb72d8c37 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 00:33:38 +0200 Subject: [PATCH 015/176] Add option to ignore cached input file --- solution_runner/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index b8ceb92f..1da0cad2 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -18,7 +18,8 @@ def cli(): @click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, get_default_year()), default=get_default_year(), help='year of challenge setting up solution for') @click.option('-d', '--day', type=click.IntRange(1, 25), help='day of challenge setting up solution for') -def setup(year: int, day: int): +@click.option('--use_cache/--ignore_cache', default=True, help='whether to use cached input file [default: true]') +def setup(year: int, day: int, use_cache: bool): """Set up a solution: fetch input and create solution files.""" From ca658e0980ea2ddff0a996412601d3dee3c439c3 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 00:43:44 +0200 Subject: [PATCH 016/176] Add Python file extension constant --- solution_runner/consts.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/solution_runner/consts.py b/solution_runner/consts.py index 10010d6e..0615f4e0 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,5 +1,11 @@ +from pathlib import Path + + FIRST_AOC_YEAR = 2015 +_PYTHON_FILE_EXTENSION = '.py' +COMMANDS_FILES_PATHS = Path('commands').glob(f'*{_PYTHON_FILE_EXTENSION}') + class CliConstants: CONTEXT = {'help_option_names': ['-h', '--help']} From 48bdddba50d23aa97d4e68f3ab193aa682efb014 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 00:46:40 +0200 Subject: [PATCH 017/176] Add Python file extension --- solution_runner/consts.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/solution_runner/consts.py b/solution_runner/consts.py index 0615f4e0..e501f757 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,10 +1,6 @@ -from pathlib import Path - - FIRST_AOC_YEAR = 2015 _PYTHON_FILE_EXTENSION = '.py' -COMMANDS_FILES_PATHS = Path('commands').glob(f'*{_PYTHON_FILE_EXTENSION}') class CliConstants: From 9ae190e1022f92a54e2050452f15969b4cb92aa6 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 00:47:56 +0200 Subject: [PATCH 018/176] Add commands files paths constant --- solution_runner/consts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/solution_runner/consts.py b/solution_runner/consts.py index e501f757..3e6537c4 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,6 +1,10 @@ +from pathlib import Path + + FIRST_AOC_YEAR = 2015 _PYTHON_FILE_EXTENSION = '.py' +COMMANDS_FILES_PATHS = tuple(Path('commands').glob(f'*{_PYTHON_FILE_EXTENSION}')) class CliConstants: From 024436ef066b9ff8658463acf539f9c99aa1fe73 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 01:18:39 +0200 Subject: [PATCH 019/176] Add separators to module notation --- solution_runner/consts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solution_runner/consts.py b/solution_runner/consts.py index 3e6537c4..65989714 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -5,6 +5,8 @@ _PYTHON_FILE_EXTENSION = '.py' COMMANDS_FILES_PATHS = tuple(Path('commands').glob(f'*{_PYTHON_FILE_EXTENSION}')) +# Linux and Windows separators to module notation, including upper package and accessing module inside package. +SEPARATORS_TO_MODULE_NOTATION = {'../': '..', '/': '.', '..\\': '..', '\\': '.'} class CliConstants: From 14e83ae2a22a5bd82721ad31c70a7f6fbc12803e Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 01:25:44 +0200 Subject: [PATCH 020/176] Create Python module for filepaths and modules utils --- solution_runner/modules_utils.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 solution_runner/modules_utils.py diff --git a/solution_runner/modules_utils.py b/solution_runner/modules_utils.py new file mode 100644 index 00000000..b970054b --- /dev/null +++ b/solution_runner/modules_utils.py @@ -0,0 +1 @@ +"""Filepaths and modules utils.""" From b7d5a2ea035a26e716edfcfe3975d2cd892dc2c5 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 01:40:14 +0200 Subject: [PATCH 021/176] Add function that converts a filepath to module form --- solution_runner/modules_utils.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/solution_runner/modules_utils.py b/solution_runner/modules_utils.py index b970054b..09b6d7e2 100644 --- a/solution_runner/modules_utils.py +++ b/solution_runner/modules_utils.py @@ -1 +1,16 @@ """Filepaths and modules utils.""" +from pathlib import Path + +import consts + + +def convert_filepath_to_relative_module(filepath: Path, directory_relative_to: Path | str = Path('.')) -> str: + """ + :param filepath: file path to convert to module + :param directory_relative_to: directory to compute relative path to from `file_path`, default is current directory + :return: module name in relative terms to `file_path` + """ + module_name = str(filepath.relative_to(directory_relative_to)) + for path_form, module_form in consts.SEPARATORS_TO_MODULE_NOTATION.values(): + module_name = module_name.replace(path_form, module_form) + return module_name From 06572e772309f732bac3503496c21c2a19906ab1 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 01:45:13 +0200 Subject: [PATCH 022/176] Add function that imports a module from a filepath --- solution_runner/modules_utils.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/solution_runner/modules_utils.py b/solution_runner/modules_utils.py index 09b6d7e2..b046ac10 100644 --- a/solution_runner/modules_utils.py +++ b/solution_runner/modules_utils.py @@ -1,16 +1,18 @@ """Filepaths and modules utils.""" +import importlib from pathlib import Path +from types import ModuleType import consts -def convert_filepath_to_relative_module(filepath: Path, directory_relative_to: Path | str = Path('.')) -> str: +def get_module_from_filepath(filepath: Path, directory_relative_to: Path | str = Path('.')) -> ModuleType: """ - :param filepath: file path to convert to module + :param filepath: file path to import :param directory_relative_to: directory to compute relative path to from `file_path`, default is current directory - :return: module name in relative terms to `file_path` + :return: imported module from `file_path` """ module_name = str(filepath.relative_to(directory_relative_to)) - for path_form, module_form in consts.SEPARATORS_TO_MODULE_NOTATION.values(): + for path_form, module_form in consts.SEPARATORS_TO_MODULE_NOTATION.items(): module_name = module_name.replace(path_form, module_form) - return module_name + return importlib.import_module(module_name) From d07aaade34d31b185041f16728432c653e216f0f Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 01:50:46 +0200 Subject: [PATCH 023/176] Use filename stem only instead of whole filename --- solution_runner/modules_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/modules_utils.py b/solution_runner/modules_utils.py index b046ac10..122fca1a 100644 --- a/solution_runner/modules_utils.py +++ b/solution_runner/modules_utils.py @@ -12,7 +12,7 @@ def get_module_from_filepath(filepath: Path, directory_relative_to: Path | str = :param directory_relative_to: directory to compute relative path to from `file_path`, default is current directory :return: imported module from `file_path` """ - module_name = str(filepath.relative_to(directory_relative_to)) + module_name = filepath.relative_to(directory_relative_to).stem for path_form, module_form in consts.SEPARATORS_TO_MODULE_NOTATION.items(): module_name = module_name.replace(path_form, module_form) return importlib.import_module(module_name) From 84e89532e52bd5117b1e4b9a23e0136954ef11c4 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 12:00:04 +0200 Subject: [PATCH 024/176] Move defaults and choices file into commands directory --- solution_runner/{ => commands}/defaults_and_choices.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename solution_runner/{ => commands}/defaults_and_choices.py (100%) diff --git a/solution_runner/defaults_and_choices.py b/solution_runner/commands/defaults_and_choices.py similarity index 100% rename from solution_runner/defaults_and_choices.py rename to solution_runner/commands/defaults_and_choices.py From 26e2ea7b49fca56c67817253cfcfb04ab283f33c Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 12:05:19 +0200 Subject: [PATCH 025/176] Create commands constants file --- solution_runner/commands/consts.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 solution_runner/commands/consts.py diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py new file mode 100644 index 00000000..e69de29b From 3e0dae07b4981a8988b8536e20a437e5656a004e Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 12:06:03 +0200 Subject: [PATCH 026/176] Move "first AoC year" constant to commands constants file --- solution_runner/commands/consts.py | 1 + solution_runner/consts.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index e69de29b..4f546604 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -0,0 +1 @@ +FIRST_AOC_YEAR = 2015 diff --git a/solution_runner/consts.py b/solution_runner/consts.py index 65989714..bc9f9821 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,8 +1,6 @@ from pathlib import Path -FIRST_AOC_YEAR = 2015 - _PYTHON_FILE_EXTENSION = '.py' COMMANDS_FILES_PATHS = tuple(Path('commands').glob(f'*{_PYTHON_FILE_EXTENSION}')) # Linux and Windows separators to module notation, including upper package and accessing module inside package. From 45802a6d37c4774ef2aa9568fa5e9e191c58cf4a Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 4 Dec 2021 12:08:51 +0200 Subject: [PATCH 027/176] Change commands files globbing to accept special suffix only This is to ignore other non-command Python files, such as constants file. --- solution_runner/consts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solution_runner/consts.py b/solution_runner/consts.py index bc9f9821..8b4387ac 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,8 +1,8 @@ from pathlib import Path -_PYTHON_FILE_EXTENSION = '.py' -COMMANDS_FILES_PATHS = tuple(Path('commands').glob(f'*{_PYTHON_FILE_EXTENSION}')) +COMMAND_FILE_SUFFIX = '_command.py' +COMMANDS_FILES_PATHS = tuple(Path('commands').glob(f'*{COMMAND_FILE_SUFFIX}')) # Linux and Windows separators to module notation, including upper package and accessing module inside package. SEPARATORS_TO_MODULE_NOTATION = {'../': '..', '/': '.', '..\\': '..', '\\': '.'} From f41bb5e8ad8ed4b67f24ff5f815162273d496f3d Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Wed, 8 Dec 2021 17:11:49 +0200 Subject: [PATCH 028/176] Add return type hint --- solution_runner/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index 1da0cad2..a19df5f1 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -6,7 +6,7 @@ @click.group(context_settings=CliConstants.CONTEXT) -def cli(): +def cli() -> click.Group: """ Main CLI holding all Advent of Code related commands. From 2e11b7f1c5f6283d06070e46c05aae40ea35b0d4 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Wed, 8 Dec 2021 17:17:08 +0200 Subject: [PATCH 029/176] Move setup command to another file --- solution_runner/cli.py | 10 ---------- solution_runner/commands/setup_command.py | 13 +++++++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 solution_runner/commands/setup_command.py diff --git a/solution_runner/cli.py b/solution_runner/cli.py index a19df5f1..1c5eaa46 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -2,7 +2,6 @@ import consts from consts import CliConstants -from defaults_and_choices import get_default_year @click.group(context_settings=CliConstants.CONTEXT) @@ -14,14 +13,5 @@ def cli() -> click.Group: """ -@cli.command() -@click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, get_default_year()), - default=get_default_year(), help='year of challenge setting up solution for') -@click.option('-d', '--day', type=click.IntRange(1, 25), help='day of challenge setting up solution for') -@click.option('--use_cache/--ignore_cache', default=True, help='whether to use cached input file [default: true]') -def setup(year: int, day: int, use_cache: bool): - """Set up a solution: fetch input and create solution files.""" - - if __name__ == '__main__': cli() diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py new file mode 100644 index 00000000..23de7097 --- /dev/null +++ b/solution_runner/commands/setup_command.py @@ -0,0 +1,13 @@ +import click + +import consts +from defaults_and_choices import get_default_year + + +@click.command(name='setup') +@click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, get_default_year()), + default=get_default_year(), help='year of challenge setting up solution for') +@click.option('-d', '--day', type=click.IntRange(1, 25), help='day of challenge setting up solution for') +@click.option('--use_cache/--ignore_cache', default=True, help='whether to use cached input file [default: true]') +def command(year: int, day: int, use_cache: bool): + """Set up a solution: fetch input and create solution files.""" From bba1781750c3e6df4efa34e6ef9f89ba5c8d7316 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Wed, 8 Dec 2021 17:22:42 +0200 Subject: [PATCH 030/176] Add function that import a subcommand from a module --- solution_runner/cli.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index 1c5eaa46..4afbdb00 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -1,7 +1,11 @@ +from pathlib import Path +from typing import Iterator + import click import consts from consts import CliConstants +from solution_runner.modules_utils import get_module_from_filepath @click.group(context_settings=CliConstants.CONTEXT) @@ -13,5 +17,16 @@ def cli() -> click.Group: """ +def _add_commands_to_cli(cli: click.Group, commands_files_paths: Iterator[Path]) -> click.Group: + """ + :param cli: click.Group object + :param commands_files_paths: path to directory containing Python files with Python subcommands + :return: CLI with added subcommands + """ + for filepath in commands_files_paths: + module = get_module_from_filepath(filepath) + cli.add_command(module.command) + + if __name__ == '__main__': cli() From f55de9d9852f914c409340c68b3b228831cb6ae2 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Wed, 8 Dec 2021 17:24:03 +0200 Subject: [PATCH 031/176] Create a main function for the solution runner --- solution_runner/cli.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index 4afbdb00..9a6ed47d 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -28,5 +28,11 @@ def _add_commands_to_cli(cli: click.Group, commands_files_paths: Iterator[Path]) cli.add_command(module.command) -if __name__ == '__main__': +def main(): + """Add subcommands to the solution runner CLI and run it.""" + _add_commands_to_cli(cli, consts.COMMANDS_FILES_PATHS) cli() + + +if __name__ == '__main__': + main() From a7af43e19f1b7043ad91225bcd5a63250369a7c0 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Wed, 8 Dec 2021 17:36:57 +0200 Subject: [PATCH 032/176] Add "should" to "use_cache" to clarify it's a boolean --- solution_runner/commands/setup_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 23de7097..a221133e 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -9,5 +9,5 @@ default=get_default_year(), help='year of challenge setting up solution for') @click.option('-d', '--day', type=click.IntRange(1, 25), help='day of challenge setting up solution for') @click.option('--use_cache/--ignore_cache', default=True, help='whether to use cached input file [default: true]') -def command(year: int, day: int, use_cache: bool): +def command(year: int, day: int, should_use_cache: bool): """Set up a solution: fetch input and create solution files.""" From c7e1f4041782584267401f13ffe49bc90b507ff4 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Wed, 8 Dec 2021 18:14:58 +0200 Subject: [PATCH 033/176] Make day option required --- solution_runner/commands/setup_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index a221133e..9c673095 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -7,7 +7,7 @@ @click.command(name='setup') @click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, get_default_year()), default=get_default_year(), help='year of challenge setting up solution for') -@click.option('-d', '--day', type=click.IntRange(1, 25), help='day of challenge setting up solution for') +@click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of challenge setting up solution for') @click.option('--use_cache/--ignore_cache', default=True, help='whether to use cached input file [default: true]') def command(year: int, day: int, should_use_cache: bool): """Set up a solution: fetch input and create solution files.""" From 5370b5cf97d33879fc9601235fe7b2264beeca2f Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Wed, 8 Dec 2021 18:16:28 +0200 Subject: [PATCH 034/176] Add session ID to options list --- solution_runner/commands/setup_command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 9c673095..2d17e53d 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -9,5 +9,6 @@ default=get_default_year(), help='year of challenge setting up solution for') @click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of challenge setting up solution for') @click.option('--use_cache/--ignore_cache', default=True, help='whether to use cached input file [default: true]') +@click.option('--session_id', envvar='AOC_SESSION_ID', prompt=True, help='session ID to access challenges input') def command(year: int, day: int, should_use_cache: bool): """Set up a solution: fetch input and create solution files.""" From 12c4e2c649b80a5ed2bb62f5d5f1e7d0a0cd352c Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Wed, 8 Dec 2021 18:18:55 +0200 Subject: [PATCH 035/176] Add session ID to command parameters --- solution_runner/commands/setup_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 2d17e53d..3529271e 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -10,5 +10,5 @@ @click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of challenge setting up solution for') @click.option('--use_cache/--ignore_cache', default=True, help='whether to use cached input file [default: true]') @click.option('--session_id', envvar='AOC_SESSION_ID', prompt=True, help='session ID to access challenges input') -def command(year: int, day: int, should_use_cache: bool): +def command(year: int, day: int, should_use_cache: bool, session_id: str): """Set up a solution: fetch input and create solution files.""" From afe02681dcec8dacd7b226428337474c2fd663a1 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 12:26:19 +0200 Subject: [PATCH 036/176] Add parameter name of use cache option --- solution_runner/commands/setup_command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 3529271e..f1017b0e 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -8,7 +8,8 @@ @click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, get_default_year()), default=get_default_year(), help='year of challenge setting up solution for') @click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of challenge setting up solution for') -@click.option('--use_cache/--ignore_cache', default=True, help='whether to use cached input file [default: true]') +@click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, + help='whether to use cached input file [default: true]') @click.option('--session_id', envvar='AOC_SESSION_ID', prompt=True, help='session ID to access challenges input') def command(year: int, day: int, should_use_cache: bool, session_id: str): """Set up a solution: fetch input and create solution files.""" From 6571b77dbdde18116a0a162b5c2f2de4ab910891 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 12:28:10 +0200 Subject: [PATCH 037/176] Document environment variable for session ID in subcommand help --- solution_runner/commands/setup_command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index f1017b0e..bc336e2e 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -10,6 +10,7 @@ @click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of challenge setting up solution for') @click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, help='whether to use cached input file [default: true]') -@click.option('--session_id', envvar='AOC_SESSION_ID', prompt=True, help='session ID to access challenges input') +@click.option('--session_id', envvar='AOC_SESSION_ID', prompt=True, + help='session ID to access challenges input [default stored in AOC_SESSION_ID environment variable]') def command(year: int, day: int, should_use_cache: bool, session_id: str): """Set up a solution: fetch input and create solution files.""" From 55a4bebded3b9ad585b1b2fecbb81d0aed981983 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 17:13:57 +0200 Subject: [PATCH 038/176] Add root directory to options list --- solution_runner/commands/setup_command.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index bc336e2e..70c42e8d 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -10,7 +10,10 @@ @click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of challenge setting up solution for') @click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, help='whether to use cached input file [default: true]') +@click.option('--root', 'root_directory', envvar='AOC_ROOT_DIRECTORY', + type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, readable=True, path_type=Path, + resolve_path=True), help='root directory of Advent of Code challenges solutions') @click.option('--session_id', envvar='AOC_SESSION_ID', prompt=True, help='session ID to access challenges input [default stored in AOC_SESSION_ID environment variable]') -def command(year: int, day: int, should_use_cache: bool, session_id: str): +def command(year: int, day: int, should_use_cache: bool, root_directory: Path, session_id: str): """Set up a solution: fetch input and create solution files.""" From 3fda426cae53d372e95a7641e8e663abce5b9de7 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 17:41:20 +0200 Subject: [PATCH 039/176] Add function that aborts script if input file exists and using cache --- solution_runner/commands/consts.py | 1 + solution_runner/commands/setup_command.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 4f546604..bce5c15e 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -1 +1,2 @@ FIRST_AOC_YEAR = 2015 +ZERO = '0' diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 70c42e8d..22f8daa8 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -17,3 +17,14 @@ help='session ID to access challenges input [default stored in AOC_SESSION_ID environment variable]') def command(year: int, day: int, should_use_cache: bool, root_directory: Path, session_id: str): """Set up a solution: fetch input and create solution files.""" + + +def _abort_input_file_already_exists(year: str, day: str): + """ + Abort the script because the input file and notify the user. + :param year: year of the challenge + :param day: day of the challenge + """ + day = day.lstrip(consts.ZERO) # Remove leading zeros for prettier printing. + click.secho(f"Input file for {year}'s day {day} challenge already exists", fg='red') + click.Context(command).abort() From 11d6e9a9f866ec0ad246f4087bfae1017a333baa Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 18:24:28 +0200 Subject: [PATCH 040/176] Create file extensions list in constants file --- solution_runner/commands/consts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index bce5c15e..8a2f72c8 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -1,2 +1,6 @@ FIRST_AOC_YEAR = 2015 ZERO = '0' + + +class FileExtensions: + pass From 6cb3e7a3aae0abeee24eaac044722ed43db5af56 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 18:27:01 +0200 Subject: [PATCH 041/176] Add Python to file extensions list --- solution_runner/commands/consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 8a2f72c8..6af199e1 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -3,4 +3,4 @@ class FileExtensions: - pass + PYTHON = '.py' From c703c29a552303fde10171c17bfd4f3453462de9 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 17:43:06 +0200 Subject: [PATCH 042/176] Add function that creates the input and solutions files --- solution_runner/commands/setup_command.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 22f8daa8..47e096b5 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -28,3 +28,16 @@ def _abort_input_file_already_exists(year: str, day: str): day = day.lstrip(consts.ZERO) # Remove leading zeros for prettier printing. click.secho(f"Input file for {year}'s day {day} challenge already exists", fg='red') click.Context(command).abort() + + +def _create_files(year_solutions_directory: Path, day: str): + """ + Create challenge directory and files. + :param year_solutions_directory: challenges solution files of the relevant year + :param day: day of the challenge + """ + solutions_directory = year_solutions_directory / day + solutions_directory.mkdir() + for part in ('1', '2'): + filepath = (solutions_directory / part).with_suffix(FileExtensions.PYTHON) + filepath.touch() From a776f90fa81f95ce4090c45cd95cde8af45885cb Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 22:47:53 +0200 Subject: [PATCH 043/176] Add prompt if user didn't enter root directory --- solution_runner/commands/setup_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 47e096b5..c75d14a9 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -10,7 +10,7 @@ @click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of challenge setting up solution for') @click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, help='whether to use cached input file [default: true]') -@click.option('--root', 'root_directory', envvar='AOC_ROOT_DIRECTORY', +@click.option('--root', 'root_directory', envvar='AOC_ROOT_DIRECTORY', prompt=True, type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, readable=True, path_type=Path, resolve_path=True), help='root directory of Advent of Code challenges solutions') @click.option('--session_id', envvar='AOC_SESSION_ID', prompt=True, From 4b72816dd11d3bf5900d142c33cb6cbad0eaf283 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 22:48:07 +0200 Subject: [PATCH 044/176] Make root directory option required --- solution_runner/commands/setup_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index c75d14a9..816dce0d 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -10,7 +10,7 @@ @click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of challenge setting up solution for') @click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, help='whether to use cached input file [default: true]') -@click.option('--root', 'root_directory', envvar='AOC_ROOT_DIRECTORY', prompt=True, +@click.option('--root', 'root_directory', required=True, envvar='AOC_ROOT_DIRECTORY', prompt=True, type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, readable=True, path_type=Path, resolve_path=True), help='root directory of Advent of Code challenges solutions') @click.option('--session_id', envvar='AOC_SESSION_ID', prompt=True, From 2f493da7abc64715c44969dee01ecd8552e1d1c9 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 23:33:07 +0200 Subject: [PATCH 045/176] Change "challenge" to "puzzle" This is the wording used on the Advent of Code website. --- solution_runner/commands/setup_command.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 816dce0d..24605708 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -6,15 +6,15 @@ @click.command(name='setup') @click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, get_default_year()), - default=get_default_year(), help='year of challenge setting up solution for') -@click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of challenge setting up solution for') + default=get_default_year(), help='year of puzzle setting up solution for') +@click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of puzzle setting up solution for') @click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, help='whether to use cached input file [default: true]') @click.option('--root', 'root_directory', required=True, envvar='AOC_ROOT_DIRECTORY', prompt=True, type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, readable=True, path_type=Path, - resolve_path=True), help='root directory of Advent of Code challenges solutions') + resolve_path=True), help='root directory of Advent of Code puzzles solutions') @click.option('--session_id', envvar='AOC_SESSION_ID', prompt=True, - help='session ID to access challenges input [default stored in AOC_SESSION_ID environment variable]') + help='session ID to access puzzles input [default stored in AOC_SESSION_ID environment variable]') def command(year: int, day: int, should_use_cache: bool, root_directory: Path, session_id: str): """Set up a solution: fetch input and create solution files.""" @@ -22,11 +22,11 @@ def command(year: int, day: int, should_use_cache: bool, root_directory: Path, s def _abort_input_file_already_exists(year: str, day: str): """ Abort the script because the input file and notify the user. - :param year: year of the challenge - :param day: day of the challenge + :param year: year of the puzzle + :param day: day of the puzzle """ day = day.lstrip(consts.ZERO) # Remove leading zeros for prettier printing. - click.secho(f"Input file for {year}'s day {day} challenge already exists", fg='red') + click.secho(f"Input file for {year}'s day {day} puzzle already exists.", fg='red') click.Context(command).abort() From e3d752a875096b6ae42b1cd968efcc067073146f Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 23:56:03 +0200 Subject: [PATCH 046/176] Add constant holding US/Eastern timezone --- solution_runner/commands/consts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 6af199e1..c345e435 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -1,6 +1,10 @@ +import datetime FIRST_AOC_YEAR = 2015 ZERO = '0' class FileExtensions: PYTHON = '.py' + + +AOC_UNLOCK_TIME_TEMPLATE = datetime.datetime(year=1, month=12, day=1, hour=0, tzinfo=US_EASTERN_TIMEZONE) From fc716898e7dd8c2995587dddfb890155cd3da42e Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 10 Dec 2021 23:59:20 +0200 Subject: [PATCH 047/176] Add function that aborts the script if the puzzle isn't unlocked yet --- solution_runner/commands/setup_command.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 24605708..99f49268 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -1,3 +1,5 @@ +from datetime import datetime + import click import consts @@ -19,6 +21,19 @@ def command(year: int, day: int, should_use_cache: bool, root_directory: Path, s """Set up a solution: fetch input and create solution files.""" +def _abort_if_puzzle_locked(year: int, day: int): + """ + Check if the puzzle at the given time wasn't unlocked yet, and abort if true. + :param year: year of the puzzle + :param day: day of the puzzle + """ + puzzle_unlock_time = consts.AOC_UNLOCK_TIME_TEMPLATE.replace(year=year, day=day) + now = datetime.now(tz=consts.US_EASTERN_TIMEZONE) + if puzzle_unlock_time > now: + click.secho(f"{year}'s day {day} puzzle wasn't unlocked yet.", fg='red') + click.Context(command).abort() + + def _abort_input_file_already_exists(year: str, day: str): """ Abort the script because the input file and notify the user. From b063f7bb11264f4d620e5613f83ccff93ef512f0 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 11 Dec 2021 00:01:01 +0200 Subject: [PATCH 048/176] Add function that asks user to create a directory if it doesn't exist --- solution_runner/commands/setup_command.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 99f49268..7b8a3620 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -34,6 +34,24 @@ def _abort_if_puzzle_locked(year: int, day: int): click.Context(command).abort() +def _ask_user_to_mkdir(directory: Path, name: str = None): + """ + Ask the user whether to create the given directory and abort if negative. + + Should be use for directories which do not exist. + :param directory: directory to ask about + :param name: name of the directory to ask the user about, given directory name is default + """ + if directory.is_dir(): + return + + if name is None: + name = directory.name + if click.confirm(f"{name} directory doesn't exist, do you want to create it?", prompt_suffix='', default=True, + show_default=True, abort=True): + directory.mkdir() + + def _abort_input_file_already_exists(year: str, day: str): """ Abort the script because the input file and notify the user. From 91852d946b8463f493f0cf82d93f811a1dfb1348 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 11 Dec 2021 19:25:15 +0200 Subject: [PATCH 049/176] Add function that downloads the challenge's input and write it to a file --- solution_runner/commands/consts.py | 5 ++++- solution_runner/commands/setup_command.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index c345e435..63a1148d 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -1,4 +1,6 @@ -import datetime +from zoneinfo import ZoneInfo + + FIRST_AOC_YEAR = 2015 ZERO = '0' @@ -7,4 +9,5 @@ class FileExtensions: PYTHON = '.py' +US_EASTERN_TIMEZONE = ZoneInfo('US/Eastern') AOC_UNLOCK_TIME_TEMPLATE = datetime.datetime(year=1, month=12, day=1, hour=0, tzinfo=US_EASTERN_TIMEZONE) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 7b8a3620..6274baf1 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -74,3 +74,22 @@ def _create_files(year_solutions_directory: Path, day: str): for part in ('1', '2'): filepath = (solutions_directory / part).with_suffix(FileExtensions.PYTHON) filepath.touch() + + +def _download_input(year: str, day: str, input_file: Path, session_id: str): + """ + Download the puzzle's input and write it to a file. + :param year: year of the puzzle + :param day: day of the puzzle + :param input_file: input file path + :param session_id: session ID to download input from website + """ + day = day.lstrip(consts.ZERO) + url = consts.INPUT_URL.format(year=year, day=day) + cookie = {'session': session_id} + + request = requests.get(url, cookies=cookie) + request.raise_for_status() + + input_text = request.text + input_file.write_text(input_text) From 97a4aa879b3ed9ecd10f8d11870d1c1603c52a82 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 11 Dec 2021 19:26:56 +0200 Subject: [PATCH 050/176] Add missing import statements --- solution_runner/commands/setup_command.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 6274baf1..9c55bda0 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -1,6 +1,12 @@ from datetime import datetime +from pathlib import Path import click +import requests + +from . import consts +from .consts import Directories, FileExtensions +from .defaults_and_choices import get_default_year import consts from defaults_and_choices import get_default_year From 4f40076d7576bdae67cca93958979e0a9619a5f5 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 11 Dec 2021 19:27:31 +0200 Subject: [PATCH 051/176] Use click's `show_default` --- solution_runner/commands/setup_command.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 9c55bda0..226a30f2 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -8,16 +8,16 @@ from .consts import Directories, FileExtensions from .defaults_and_choices import get_default_year -import consts -from defaults_and_choices import get_default_year + +_default_year = get_default_year() @click.command(name='setup') -@click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, get_default_year()), - default=get_default_year(), help='year of puzzle setting up solution for') +@click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), default=_default_year, + show_default=f'last year: {_default_year}', help='year of puzzle setting up solution for') @click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of puzzle setting up solution for') -@click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, - help='whether to use cached input file [default: true]') +@click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, show_default='true', + help='whether to use cached input file') @click.option('--root', 'root_directory', required=True, envvar='AOC_ROOT_DIRECTORY', prompt=True, type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, readable=True, path_type=Path, resolve_path=True), help='root directory of Advent of Code puzzles solutions') From e2f5b5313e0501c16075607bd19cb8d5c811056a Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 11 Dec 2021 19:28:34 +0200 Subject: [PATCH 052/176] Add text file extension --- solution_runner/commands/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 63a1148d..6c980c42 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -6,6 +6,7 @@ class FileExtensions: + TEXT = '.txt' PYTHON = '.py' From 38857bf70f154c0ee943cbcd78c56e7ec0c961fb Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 18 Dec 2021 22:54:08 +0200 Subject: [PATCH 053/176] Remove suffix instead of calling `stem()` `Path.stem()` removes the parents of the file too, but they're needed for the import. --- solution_runner/modules_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/modules_utils.py b/solution_runner/modules_utils.py index 122fca1a..eaad3ff1 100644 --- a/solution_runner/modules_utils.py +++ b/solution_runner/modules_utils.py @@ -12,7 +12,7 @@ def get_module_from_filepath(filepath: Path, directory_relative_to: Path | str = :param directory_relative_to: directory to compute relative path to from `file_path`, default is current directory :return: imported module from `file_path` """ - module_name = filepath.relative_to(directory_relative_to).stem + module_name = str(filepath.relative_to(directory_relative_to).with_suffix('')) for path_form, module_form in consts.SEPARATORS_TO_MODULE_NOTATION.items(): module_name = module_name.replace(path_form, module_form) return importlib.import_module(module_name) From 936a9e0a32b8045c4af2f59b0c0c1715a1a44248 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 19 Dec 2021 19:07:32 +0200 Subject: [PATCH 054/176] Create class containing important directory paths --- solution_runner/commands/consts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 6c980c42..e2e56a86 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -5,6 +5,10 @@ ZERO = '0' +class Directories: + pass + + class FileExtensions: TEXT = '.txt' PYTHON = '.py' From 31e3e49428b50f29aade7cb185b635f3ef325f9b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 19 Dec 2021 19:08:05 +0200 Subject: [PATCH 055/176] Add solutions directory path --- solution_runner/commands/consts.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index e2e56a86..2c054f01 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -1,12 +1,15 @@ +from pathlib import Path from zoneinfo import ZoneInfo FIRST_AOC_YEAR = 2015 ZERO = '0' +INPUT_URL = 'https://adventofcode.com/{year}/day/{day}/input' +COOKIE = {'session': None} class Directories: - pass + SOLUTIONS = Path('solutions') class FileExtensions: From 623a455276adf84bcf8884dee2ee358bf98eb2c0 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 19 Dec 2021 19:08:23 +0200 Subject: [PATCH 056/176] Add input files directory path --- solution_runner/commands/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 2c054f01..445c1926 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -10,6 +10,7 @@ class Directories: SOLUTIONS = Path('solutions') + INPUTS = Path('inputs') class FileExtensions: From eb71e53260b30a8ae078d009ee07890b2e3d1cb1 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 23 Dec 2021 17:58:04 +0200 Subject: [PATCH 057/176] Add import for unlock time template --- solution_runner/commands/consts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 445c1926..858ab89d 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -1,3 +1,4 @@ +from datetime import datetime from pathlib import Path from zoneinfo import ZoneInfo @@ -19,4 +20,4 @@ class FileExtensions: US_EASTERN_TIMEZONE = ZoneInfo('US/Eastern') -AOC_UNLOCK_TIME_TEMPLATE = datetime.datetime(year=1, month=12, day=1, hour=0, tzinfo=US_EASTERN_TIMEZONE) +AOC_UNLOCK_TIME_TEMPLATE = datetime(year=1, month=12, day=1, hour=0, tzinfo=US_EASTERN_TIMEZONE) From a8527899a944c50b61d1ddc04c431234e9496288 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 23 Dec 2021 18:00:39 +0200 Subject: [PATCH 058/176] Use same package import --- solution_runner/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index 9a6ed47d..a557f049 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -5,7 +5,7 @@ import consts from consts import CliConstants -from solution_runner.modules_utils import get_module_from_filepath +from modules_utils import get_module_from_filepath @click.group(context_settings=CliConstants.CONTEXT) From 011e595dccae7d200032b3b61f66265116c96a76 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 23 Dec 2021 18:12:45 +0200 Subject: [PATCH 059/176] Create config command --- solution_runner/commands/config_command.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 solution_runner/commands/config_command.py diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py new file mode 100644 index 00000000..cda7a156 --- /dev/null +++ b/solution_runner/commands/config_command.py @@ -0,0 +1,7 @@ +import click + + +@click.command(name='config') +def command(): + """Set options.""" + pass From afa4c527c0456ad3c5d17ae71dedfc5c2143c55c Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 23 Dec 2021 18:14:07 +0200 Subject: [PATCH 060/176] Add root directory parameter --- solution_runner/commands/config_command.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index cda7a156..e1b0e34a 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -1,7 +1,12 @@ +from pathlib import Path + import click @click.command(name='config') -def command(): +@click.option('--root', 'root_directory', + type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, readable=True, path_type=Path, + resolve_path=True), help='root directory of Advent of Code puzzles project') +def command(root_directory: Path): """Set options.""" pass From 61e974a80abdf88e4e9d2bd78acbdc99516fd82a Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 23 Dec 2021 18:14:47 +0200 Subject: [PATCH 061/176] Add session ID parameter --- solution_runner/commands/config_command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index e1b0e34a..130e7acd 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -7,6 +7,7 @@ @click.option('--root', 'root_directory', type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, readable=True, path_type=Path, resolve_path=True), help='root directory of Advent of Code puzzles project') -def command(root_directory: Path): +@click.option('--session-id', help='session ID to access puzzles input') +def command(root_directory: Path, session_id: str): """Set options.""" pass From 3848c53fa0c3c7a48740375cf8b7fda967f477bd Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 23 Dec 2021 18:23:28 +0200 Subject: [PATCH 062/176] Add PyYAML to Python packages requirements file --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 23adf640..fa3b6c57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ requests -click \ No newline at end of file +click +pyyaml \ No newline at end of file From d9c68dd0e31b96122554e20a98108cb958de84f5 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 24 Dec 2021 20:13:48 +0200 Subject: [PATCH 063/176] Add generic and cool config command --- solution_runner/commands/config_command.py | 29 +++++++++++++++++++++- solution_runner/commands/consts.py | 6 +++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index 130e7acd..e6bd8952 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -1,6 +1,9 @@ from pathlib import Path import click +import yaml + +import consts @click.command(name='config') @@ -10,4 +13,28 @@ @click.option('--session-id', help='session ID to access puzzles input') def command(root_directory: Path, session_id: str): """Set options.""" - pass + app_data_directory = Path(click.get_app_dir('Advent of Code')) + configuration_file = Path(app_data_directory, consts.CONFIGURATION_FILE_NAME) + try: + configuration = yaml.safe_load(configuration_file.read_text()) or {} + configuration_changed = False + except FileNotFoundError: + app_data_directory.mkdir(exist_ok=True) + configuration_file.touch() + configuration = {} + configuration_changed = True + + for key, (explanation, check_type, stored_type, hide_input) in consts.CONFIGURATION_KEY_TO_PARAMETERS.items(): + if key in configuration: + continue + + configuration_changed = True + value = click.prompt(f'Enter {explanation}', hide_input=hide_input, type=check_type) + value = stored_type(value) + configuration[key] = value + + if configuration_changed: + configuration_file.write_text(yaml.dump(configuration)) + + +command() diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 858ab89d..7be55587 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -8,6 +8,12 @@ INPUT_URL = 'https://adventofcode.com/{year}/day/{day}/input' COOKIE = {'session': None} +CONFIGURATION_KEY_TO_PARAMETERS = { + # Explanation, type to use to check the value against, stored type in configuration file, whether to hide input. + 'root_directory': ('root directory of Advent of Code project', Path, str, False), + 'session_id': ('session ID to download input files (can be accessed from AoC website cookies', str, str, True) +} + class Directories: SOLUTIONS = Path('solutions') From 78789ea223644acf727533a6dd7547c4fe1c848a Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 24 Dec 2021 20:14:36 +0200 Subject: [PATCH 064/176] Revert "Add generic and cool config command" This reverts commit 89cb168a02d32f0db0da75e775452b6aa1625a14. --- solution_runner/commands/config_command.py | 29 +--------------------- solution_runner/commands/consts.py | 6 ----- 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index e6bd8952..130e7acd 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -1,9 +1,6 @@ from pathlib import Path import click -import yaml - -import consts @click.command(name='config') @@ -13,28 +10,4 @@ @click.option('--session-id', help='session ID to access puzzles input') def command(root_directory: Path, session_id: str): """Set options.""" - app_data_directory = Path(click.get_app_dir('Advent of Code')) - configuration_file = Path(app_data_directory, consts.CONFIGURATION_FILE_NAME) - try: - configuration = yaml.safe_load(configuration_file.read_text()) or {} - configuration_changed = False - except FileNotFoundError: - app_data_directory.mkdir(exist_ok=True) - configuration_file.touch() - configuration = {} - configuration_changed = True - - for key, (explanation, check_type, stored_type, hide_input) in consts.CONFIGURATION_KEY_TO_PARAMETERS.items(): - if key in configuration: - continue - - configuration_changed = True - value = click.prompt(f'Enter {explanation}', hide_input=hide_input, type=check_type) - value = stored_type(value) - configuration[key] = value - - if configuration_changed: - configuration_file.write_text(yaml.dump(configuration)) - - -command() + pass diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 7be55587..858ab89d 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -8,12 +8,6 @@ INPUT_URL = 'https://adventofcode.com/{year}/day/{day}/input' COOKIE = {'session': None} -CONFIGURATION_KEY_TO_PARAMETERS = { - # Explanation, type to use to check the value against, stored type in configuration file, whether to hide input. - 'root_directory': ('root directory of Advent of Code project', Path, str, False), - 'session_id': ('session ID to download input files (can be accessed from AoC website cookies', str, str, True) -} - class Directories: SOLUTIONS = Path('solutions') From fd69ac0c3848facd7b478fe27e2a1505379b82d9 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Wed, 29 Dec 2021 20:44:26 +0200 Subject: [PATCH 065/176] Add root directory Click path --- solution_runner/commands/consts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 858ab89d..9e5b7267 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -2,12 +2,16 @@ from pathlib import Path from zoneinfo import ZoneInfo +import click + FIRST_AOC_YEAR = 2015 ZERO = '0' INPUT_URL = 'https://adventofcode.com/{year}/day/{day}/input' COOKIE = {'session': None} +ROOT_DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True) + class Directories: SOLUTIONS = Path('solutions') From 20864edcaee87d58f67189fef639e498099428c4 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 21:57:53 +0200 Subject: [PATCH 066/176] Change root directory parameter type to new constant --- solution_runner/commands/config_command.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index 130e7acd..4c89d231 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -4,10 +4,9 @@ @click.command(name='config') -@click.option('--root', 'root_directory', - type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, readable=True, path_type=Path, - resolve_path=True), help='root directory of Advent of Code puzzles project') +@click.option('--root', 'root_directory', type=consts.ROOT_DIRECTORY_TYPE, prompt=True, prompt_required=False, + help='root directory of Advent of Code puzzles project') @click.option('--session-id', help='session ID to access puzzles input') -def command(root_directory: Path, session_id: str): +def command(root_directory: str, session_id: str): """Set options.""" pass From 44a729ad67694eb892f2d666a38753f395e280aa Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:05:58 +0200 Subject: [PATCH 067/176] Add CLI app data directory that stores configuration file constant --- solution_runner/commands/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 9e5b7267..f1a26d77 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -10,6 +10,7 @@ INPUT_URL = 'https://adventofcode.com/{year}/day/{day}/input' COOKIE = {'session': None} +APP_DATA_DIRECTORY = click.get_app_dir('Advent of Code') ROOT_DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True) From eecd20c12faad99a52f7f2bf0adba7663b667530 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:19:28 +0200 Subject: [PATCH 068/176] Create app data directory variable --- solution_runner/commands/config_command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index 4c89d231..57afeab8 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -2,6 +2,8 @@ import click +import consts + @click.command(name='config') @click.option('--root', 'root_directory', type=consts.ROOT_DIRECTORY_TYPE, prompt=True, prompt_required=False, @@ -9,4 +11,4 @@ @click.option('--session-id', help='session ID to access puzzles input') def command(root_directory: str, session_id: str): """Set options.""" - pass + app_data_directory = click.get_app_dir(consts.APP_DATA_DIRECTORY) From c422fdabb4385a8dc2dd3d02e95dcec9ec84f566 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:40:17 +0200 Subject: [PATCH 069/176] Create configuration file name constant --- solution_runner/commands/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index f1a26d77..8d6c22c3 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -11,6 +11,7 @@ COOKIE = {'session': None} APP_DATA_DIRECTORY = click.get_app_dir('Advent of Code') +CONFIGURATION_FILE_NAME = Path('configuration.yaml') ROOT_DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True) From db9e7bdfcdab9b0e405484eadd2f34f6ee9ca77d Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:36:12 +0200 Subject: [PATCH 070/176] Create configuration file variable --- solution_runner/commands/config_command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index 57afeab8..5a2d23b5 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -12,3 +12,4 @@ def command(root_directory: str, session_id: str): """Set options.""" app_data_directory = click.get_app_dir(consts.APP_DATA_DIRECTORY) + configuration_file = Path(app_data_directory, consts.CONFIGURATION_FILE_NAME) From 52ca64fc3665ad27ec862ee3451fafb0a2336cbf Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:37:31 +0200 Subject: [PATCH 071/176] Read from configuration file and create it and its directory if needed --- solution_runner/commands/config_command.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index 5a2d23b5..11a91c5b 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -1,6 +1,7 @@ from pathlib import Path import click +import yaml import consts @@ -13,3 +14,10 @@ def command(root_directory: str, session_id: str): """Set options.""" app_data_directory = click.get_app_dir(consts.APP_DATA_DIRECTORY) configuration_file = Path(app_data_directory, consts.CONFIGURATION_FILE_NAME) + try: + # Use an empty dictionary if no actual configuration is in the configuration file. + configuration = yaml.safe_load(configuration_file.read_text()) or {} + except FileNotFoundError: + Path(app_data_directory).mkdir(exist_ok=True) + configuration_file.touch() + configuration = {} From da87fac5faa18dd248d71e6fe9aa66c9d6bd1b71 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:39:20 +0200 Subject: [PATCH 072/176] Save the initial configuration to see if it was edited afterwards --- solution_runner/commands/config_command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index 11a91c5b..90a909e5 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -21,3 +21,4 @@ def command(root_directory: str, session_id: str): Path(app_data_directory).mkdir(exist_ok=True) configuration_file.touch() configuration = {} + initial_configuration = configuration.copy() From 72239d44b258742abb469205c01f50db959516c1 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 00:28:47 +0200 Subject: [PATCH 073/176] Add root directory key name --- solution_runner/commands/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 8d6c22c3..67205bda 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -13,6 +13,7 @@ APP_DATA_DIRECTORY = click.get_app_dir('Advent of Code') CONFIGURATION_FILE_NAME = Path('configuration.yaml') ROOT_DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True) +ROOT_DIRECTORY = 'root directory' class Directories: From eac5540796437c618c9507f99e596b0f583943f6 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:44:21 +0200 Subject: [PATCH 074/176] Add function that edits the root directory configuration --- solution_runner/commands/config_command.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index 90a909e5..8d332c4d 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -1,4 +1,5 @@ from pathlib import Path +from typing import Any import click import yaml @@ -22,3 +23,21 @@ def command(root_directory: str, session_id: str): configuration_file.touch() configuration = {} initial_configuration = configuration.copy() + + root_directory = _configure_root_directory(configuration, root_directory) + root_directory = Path(root_directory) + + +def _configure_root_directory(configuration: dict[str, Any], root_directory: str | None) -> str: + """ + Edit the root directory configuration if needed. + :param configuration: current configuration + :param root_directory: root directory passed by the user, `None` if wasn't passed + :return: root directory after configuration if needed + """ + if root_directory is not None: + configuration[consts.ROOT_DIRECTORY] = root_directory + elif consts.ROOT_DIRECTORY not in configuration: + configuration[consts.ROOT_DIRECTORY] = click.prompt('Enter path for Advent of Code project root directory', + type=consts.ROOT_DIRECTORY_TYPE) + return configuration[consts.ROOT_DIRECTORY] From 997243358be967932a6d4fdfb7e48fcc96128b49 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:46:19 +0200 Subject: [PATCH 075/176] Create root directory if needed --- solution_runner/commands/config_command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index 8d332c4d..c266f9e3 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -26,6 +26,7 @@ def command(root_directory: str, session_id: str): root_directory = _configure_root_directory(configuration, root_directory) root_directory = Path(root_directory) + root_directory.mkdir(exist_ok=True) def _configure_root_directory(configuration: dict[str, Any], root_directory: str | None) -> str: From 6c1ecd34c5555e7cc80cf7928e0f1dfd3ae09bea Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:57:41 +0200 Subject: [PATCH 076/176] Add hidden prompt for session ID --- solution_runner/commands/config_command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index c266f9e3..aeaacdf4 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -10,7 +10,8 @@ @click.command(name='config') @click.option('--root', 'root_directory', type=consts.ROOT_DIRECTORY_TYPE, prompt=True, prompt_required=False, help='root directory of Advent of Code puzzles project') -@click.option('--session-id', help='session ID to access puzzles input') +@click.option('--session-id', prompt=True, prompt_required=False, hide_input=True, + help='session ID to access puzzles input') def command(root_directory: str, session_id: str): """Set options.""" app_data_directory = click.get_app_dir(consts.APP_DATA_DIRECTORY) From 997c92b69babea73be92a1cea51ea0f06703d332 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 00:29:04 +0200 Subject: [PATCH 077/176] Add session ID key name --- solution_runner/commands/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 67205bda..23f3f6cf 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -14,6 +14,7 @@ CONFIGURATION_FILE_NAME = Path('configuration.yaml') ROOT_DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True) ROOT_DIRECTORY = 'root directory' +SESSION_ID = 'session ID' class Directories: From 0b8af2e2d1b7b0f35d1c75e3a566f08895ebf3ea Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:58:09 +0200 Subject: [PATCH 078/176] Add function that edits the session ID configuration --- solution_runner/commands/config_command.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index aeaacdf4..65384a8f 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -43,3 +43,16 @@ def _configure_root_directory(configuration: dict[str, Any], root_directory: str configuration[consts.ROOT_DIRECTORY] = click.prompt('Enter path for Advent of Code project root directory', type=consts.ROOT_DIRECTORY_TYPE) return configuration[consts.ROOT_DIRECTORY] + + +def _configure_session_id(configuration: dict[str, Any], session_id: str | None): + """ + Configure the session ID if needed. + :param configuration: current configuration + :param session_id: session ID passed by the user, `None` if wasn't passed + """ + if session_id is not None: + configuration[consts.SESSION_ID] = session_id + elif consts.SESSION_ID not in configuration: + configuration[consts.SESSION_ID] = click.prompt( + 'Enter session ID to download input files (available in AoC website cookies)', hide_input=True) From f7def1e180b8d71e768d8f11ce438d18313c38b2 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:59:04 +0200 Subject: [PATCH 079/176] Configure session ID --- solution_runner/commands/config_command.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index 65384a8f..432ca941 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -29,6 +29,8 @@ def command(root_directory: str, session_id: str): root_directory = Path(root_directory) root_directory.mkdir(exist_ok=True) + _configure_session_id(configuration, session_id) + def _configure_root_directory(configuration: dict[str, Any], root_directory: str | None) -> str: """ From 2739e3a9a2fe2264b632b8fa608d2f07096e0e92 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Thu, 6 Jan 2022 22:59:37 +0200 Subject: [PATCH 080/176] Write new configuration to file if it was changed --- solution_runner/commands/config_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index 432ca941..faaf2dbb 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -31,6 +31,9 @@ def command(root_directory: str, session_id: str): _configure_session_id(configuration, session_id) + if configuration != initial_configuration: + configuration_file.write_text(yaml.dump(configuration)) + def _configure_root_directory(configuration: dict[str, Any], root_directory: str | None) -> str: """ From cf536d2c2bc62a7bd0304889af5d91c5a9740098 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 00:17:27 +0200 Subject: [PATCH 081/176] Create command file lists from absolute path --- solution_runner/consts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/consts.py b/solution_runner/consts.py index 8b4387ac..def98e4a 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -2,7 +2,8 @@ COMMAND_FILE_SUFFIX = '_command.py' -COMMANDS_FILES_PATHS = tuple(Path('commands').glob(f'*{COMMAND_FILE_SUFFIX}')) +_COMMANDS_DIRECTORY_PATH = Path(__file__).parent / Path('commands') +COMMANDS_FILES_PATHS = tuple(_COMMANDS_DIRECTORY_PATH.glob(f'*{COMMAND_FILE_SUFFIX}')) # Linux and Windows separators to module notation, including upper package and accessing module inside package. SEPARATORS_TO_MODULE_NOTATION = {'../': '..', '/': '.', '..\\': '..', '\\': '.'} From a87c6bb99ebfc35a4eac860deb962b2d825b37e0 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 00:18:42 +0200 Subject: [PATCH 082/176] Temporarily changed to CLI directory so relative imports will work --- solution_runner/cli.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index a557f049..bfcd5ab2 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -1,3 +1,4 @@ +import os from pathlib import Path from typing import Iterator @@ -23,9 +24,13 @@ def _add_commands_to_cli(cli: click.Group, commands_files_paths: Iterator[Path]) :param commands_files_paths: path to directory containing Python files with Python subcommands :return: CLI with added subcommands """ + file_directory = Path(__file__).parent + current_directory = Path.cwd() + os.chdir(file_directory) for filepath in commands_files_paths: module = get_module_from_filepath(filepath) cli.add_command(module.command) + os.chdir(current_directory) def main(): From 57e8d1858eb5083d105e6b1f169b04f39474606b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 00:20:41 +0200 Subject: [PATCH 083/176] Change import format so it'll work --- solution_runner/commands/config_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index faaf2dbb..f2d05704 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -4,7 +4,7 @@ import click import yaml -import consts +from . import consts @click.command(name='config') From 382605e1c8453f3746b967571e11ba54d836da93 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 00:25:31 +0200 Subject: [PATCH 084/176] Use absolute path of directory when processing relative path to it --- solution_runner/modules_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/modules_utils.py b/solution_runner/modules_utils.py index eaad3ff1..dc6ff81c 100644 --- a/solution_runner/modules_utils.py +++ b/solution_runner/modules_utils.py @@ -12,7 +12,7 @@ def get_module_from_filepath(filepath: Path, directory_relative_to: Path | str = :param directory_relative_to: directory to compute relative path to from `file_path`, default is current directory :return: imported module from `file_path` """ - module_name = str(filepath.relative_to(directory_relative_to).with_suffix('')) + module_name = str(filepath.relative_to(directory_relative_to.absolute()).with_suffix('')) for path_form, module_form in consts.SEPARATORS_TO_MODULE_NOTATION.items(): module_name = module_name.replace(path_form, module_form) return importlib.import_module(module_name) From 71c62c7374cd212d6ab4463f3095f6733417a89f Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 00:26:33 +0200 Subject: [PATCH 085/176] Remove configuration options from `setup` command These are now configured through the `config` command. --- solution_runner/commands/setup_command.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 226a30f2..dda9daa6 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -18,12 +18,7 @@ @click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of puzzle setting up solution for') @click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, show_default='true', help='whether to use cached input file') -@click.option('--root', 'root_directory', required=True, envvar='AOC_ROOT_DIRECTORY', prompt=True, - type=click.Path(exists=True, file_okay=False, dir_okay=True, writable=True, readable=True, path_type=Path, - resolve_path=True), help='root directory of Advent of Code puzzles solutions') -@click.option('--session_id', envvar='AOC_SESSION_ID', prompt=True, - help='session ID to access puzzles input [default stored in AOC_SESSION_ID environment variable]') -def command(year: int, day: int, should_use_cache: bool, root_directory: Path, session_id: str): +def command(year: int, day: int, should_use_cache: bool): """Set up a solution: fetch input and create solution files.""" From 226fc7c65c18ba95a1b5e9dbb6117a6d90226eb4 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 00:27:47 +0200 Subject: [PATCH 086/176] Generalize utils file --- solution_runner/cli.py | 2 +- solution_runner/{modules_utils.py => utils.py} | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) rename solution_runner/{modules_utils.py => utils.py} (95%) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index bfcd5ab2..3b522ea8 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -6,7 +6,7 @@ import consts from consts import CliConstants -from modules_utils import get_module_from_filepath +from utils import get_module_from_filepath @click.group(context_settings=CliConstants.CONTEXT) diff --git a/solution_runner/modules_utils.py b/solution_runner/utils.py similarity index 95% rename from solution_runner/modules_utils.py rename to solution_runner/utils.py index dc6ff81c..99453c1f 100644 --- a/solution_runner/modules_utils.py +++ b/solution_runner/utils.py @@ -1,4 +1,3 @@ -"""Filepaths and modules utils.""" import importlib from pathlib import Path from types import ModuleType From 3f1344a83fe763fc4f4f959197c8422620cd187f Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 17:47:20 +0200 Subject: [PATCH 087/176] Create function that returns a given key's configured value --- solution_runner/utils.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/solution_runner/utils.py b/solution_runner/utils.py index 99453c1f..50373bf4 100644 --- a/solution_runner/utils.py +++ b/solution_runner/utils.py @@ -1,7 +1,12 @@ import importlib from pathlib import Path from types import ModuleType +from typing import Any +import click +import yaml + +import commands.consts import consts @@ -15,3 +20,18 @@ def get_module_from_filepath(filepath: Path, directory_relative_to: Path | str = for path_form, module_form in consts.SEPARATORS_TO_MODULE_NOTATION.items(): module_name = module_name.replace(path_form, module_form) return importlib.import_module(module_name) + + +def get_setting(key: str) -> Any: + """ + :param key: key to retrieve its value + :return: value of `key` which is stored in the configuration file + """ + configuration_file = commands.consts.APP_DATA_DIRECTORY / commands.consts.CONFIGURATION_FILE_NAME + try: + configuration = yaml.safe_load(configuration_file.read_text()) + except FileNotFoundError: + click.secho("configuration file doesn't exist. Run config command first", fg='red') + raise + + return configuration[key] From 24e98bb9505d70481690496d27843243e0f83d7d Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 18:00:00 +0200 Subject: [PATCH 088/176] Abort is requested puzzle is still locked --- solution_runner/commands/setup_command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index dda9daa6..7c34d864 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -20,6 +20,7 @@ help='whether to use cached input file') def command(year: int, day: int, should_use_cache: bool): """Set up a solution: fetch input and create solution files.""" + _abort_if_puzzle_locked(year, day) def _abort_if_puzzle_locked(year: int, day: int): From 64e876be5ad0d491b326e337fd9febf008f2ea03 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 18:02:14 +0200 Subject: [PATCH 089/176] Convert year to string --- solution_runner/commands/setup_command.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 7c34d864..ab32a7f5 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -22,6 +22,8 @@ def command(year: int, day: int, should_use_cache: bool): """Set up a solution: fetch input and create solution files.""" _abort_if_puzzle_locked(year, day) + year = str(year) + def _abort_if_puzzle_locked(year: int, day: int): """ From 715254c2f559feeff88f607893793bc3ff6de25d Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 18:03:08 +0200 Subject: [PATCH 090/176] Convert day to string with leading zero if needed --- solution_runner/commands/setup_command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index ab32a7f5..b24af591 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -23,6 +23,7 @@ def command(year: int, day: int, should_use_cache: bool): _abort_if_puzzle_locked(year, day) year = str(year) + day = str(day).zfill(2) # Add a leading zero for single digit numbers. def _abort_if_puzzle_locked(year: int, day: int): From 412a5dc9c24f5934132105d9dce6c299cdcef502 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 18:03:36 +0200 Subject: [PATCH 091/176] Get root directory from configuration --- solution_runner/commands/setup_command.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index b24af591..f0969b99 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -7,6 +7,7 @@ from . import consts from .consts import Directories, FileExtensions from .defaults_and_choices import get_default_year +from .. import utils _default_year = get_default_year() @@ -25,6 +26,11 @@ def command(year: int, day: int, should_use_cache: bool): year = str(year) day = str(day).zfill(2) # Add a leading zero for single digit numbers. + try: + root_directory = utils.get_setting(consts.ROOT_DIRECTORY) + except FileNotFoundError: + click.Context(command).abort() + def _abort_if_puzzle_locked(year: int, day: int): """ From 999370f36635b7c0c74259ae1c93a1cdd1d4f2d2 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 18:04:11 +0200 Subject: [PATCH 092/176] Prompt use to create inputs directory if needed --- solution_runner/commands/setup_command.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index f0969b99..16cebdbb 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -30,6 +30,8 @@ def command(year: int, day: int, should_use_cache: bool): root_directory = utils.get_setting(consts.ROOT_DIRECTORY) except FileNotFoundError: click.Context(command).abort() + inputs_directory = root_directory / Directories.INPUTS + _ask_user_to_mkdir(inputs_directory, 'input files') def _abort_if_puzzle_locked(year: int, day: int): From 5b165539a9fcebad12d49f7e4864614f1ee37545 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 18:04:47 +0200 Subject: [PATCH 093/176] Prompt user to create year inputs directory if needed --- solution_runner/commands/setup_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 16cebdbb..8f4c2853 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -33,6 +33,9 @@ def command(year: int, day: int, should_use_cache: bool): inputs_directory = root_directory / Directories.INPUTS _ask_user_to_mkdir(inputs_directory, 'input files') + year_inputs_directory = inputs_directory / year + _ask_user_to_mkdir(year_inputs_directory, f'{year} input files') + def _abort_if_puzzle_locked(year: int, day: int): """ From 9ede19348beeb821434893edb1394b43534dd9d7 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 18:05:13 +0200 Subject: [PATCH 094/176] Create variable for day's input file --- solution_runner/commands/setup_command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 8f4c2853..486c9c3b 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -35,6 +35,7 @@ def command(year: int, day: int, should_use_cache: bool): year_inputs_directory = inputs_directory / year _ask_user_to_mkdir(year_inputs_directory, f'{year} input files') + input_file = (year_inputs_directory / day).with_suffix(FileExtensions.TEXT) def _abort_if_puzzle_locked(year: int, day: int): From 50a719bce190abb91ea77abd43796420e015a5d6 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 19:15:43 +0200 Subject: [PATCH 095/176] Create input file --- solution_runner/commands/setup_command.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 486c9c3b..f5c628cd 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -37,6 +37,10 @@ def command(year: int, day: int, should_use_cache: bool): _ask_user_to_mkdir(year_inputs_directory, f'{year} input files') input_file = (year_inputs_directory / day).with_suffix(FileExtensions.TEXT) + if should_use_cache and input_file.exists() and input_file.read_text(): # Abort if the file exists but it's empty. + _abort_input_file_already_exists(year, day) + input_file.touch() + def _abort_if_puzzle_locked(year: int, day: int): """ From 05594f2d280d2b7a7546688456cd6f72a1052586 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 19:17:32 +0200 Subject: [PATCH 096/176] Get session ID from configuration --- solution_runner/commands/setup_command.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index f5c628cd..b5e09e73 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -41,6 +41,11 @@ def command(year: int, day: int, should_use_cache: bool): _abort_input_file_already_exists(year, day) input_file.touch() + try: + session_id = utils.get_setting(consts.SESSION_ID) + except FileNotFoundError: + click.Context(command).abort() + def _abort_if_puzzle_locked(year: int, day: int): """ From 227459d6572cafd0465f8777f159ce3b8759d955 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 19:23:13 +0200 Subject: [PATCH 097/176] Download input to file --- solution_runner/commands/setup_command.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index b5e09e73..2f250277 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -46,6 +46,8 @@ def command(year: int, day: int, should_use_cache: bool): except FileNotFoundError: click.Context(command).abort() + _download_input(year, day, input_file, session_id) + def _abort_if_puzzle_locked(year: int, day: int): """ From bf976c17b685c5da33ec560164d12696450a229b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 7 Jan 2022 19:24:13 +0200 Subject: [PATCH 098/176] Prompt user to create day's solutions directory if needed --- solution_runner/commands/setup_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 2f250277..9f19c1e4 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -48,6 +48,9 @@ def command(year: int, day: int, should_use_cache: bool): _download_input(year, day, input_file, session_id) + solutions_directory = root_directory / Directories.SOLUTIONS + _ask_user_to_mkdir(solutions_directory, 'solution files') + def _abort_if_puzzle_locked(year: int, day: int): """ From 0e618b8a7b8b47fd64bb12248b05fd60957b6c34 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sat, 15 Jan 2022 19:32:22 +0200 Subject: [PATCH 099/176] Add Jinja to Python packages requirements file --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fa3b6c57..fa7b4464 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests click -pyyaml \ No newline at end of file +pyyaml +jinja2 \ No newline at end of file From edce6fc1f4c1f7df0b8fa2b7ff3df7af92976354 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 19:20:19 +0200 Subject: [PATCH 100/176] Add template solution file --- solution_runner/commands/solution_template.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 solution_runner/commands/solution_template.py diff --git a/solution_runner/commands/solution_template.py b/solution_runner/commands/solution_template.py new file mode 100644 index 00000000..3479ef77 --- /dev/null +++ b/solution_runner/commands/solution_template.py @@ -0,0 +1,2 @@ +def get_answer(input_text: str): + raise NotImplementedError From 9e6fa678356186368b291d8f85d6442c671c212f Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 19:20:49 +0200 Subject: [PATCH 101/176] Add constant holding solution file content --- solution_runner/commands/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 23f3f6cf..c865c7b4 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -15,6 +15,7 @@ ROOT_DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True) ROOT_DIRECTORY = 'root directory' SESSION_ID = 'session ID' +SOLUTION_FILE_CONTENT = Path('solution_template.py').read_text() class Directories: From b8309e380c782e3a88f84723d3a32849ec640b3f Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 19:27:03 +0200 Subject: [PATCH 102/176] Write solution template to solution files instead of blank --- solution_runner/commands/setup_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 9f19c1e4..076169ec 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -104,7 +104,7 @@ def _create_files(year_solutions_directory: Path, day: str): solutions_directory.mkdir() for part in ('1', '2'): filepath = (solutions_directory / part).with_suffix(FileExtensions.PYTHON) - filepath.touch() + filepath.write_text(consts.SOLUTION_FILE_CONTENT) def _download_input(year: str, day: str, input_file: Path, session_id: str): From 6cbc7545ca7e6f369bfe8b321cf849ed871fc500 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 19:29:02 +0200 Subject: [PATCH 103/176] Prompt user to create year solutions directory if needed --- solution_runner/commands/setup_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 076169ec..1a820b35 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -51,6 +51,9 @@ def command(year: int, day: int, should_use_cache: bool): solutions_directory = root_directory / Directories.SOLUTIONS _ask_user_to_mkdir(solutions_directory, 'solution files') + year_solutions_directory = solutions_directory / year + _ask_user_to_mkdir(year_solutions_directory, f'{year} solution files') + def _abort_if_puzzle_locked(year: int, day: int): """ From 0c1770f7ad1a4252ef6e14e6b6a0eb1b9f34e3fa Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 19:29:24 +0200 Subject: [PATCH 104/176] Create day solution files --- solution_runner/commands/setup_command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 1a820b35..95119ff1 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -53,6 +53,7 @@ def command(year: int, day: int, should_use_cache: bool): year_solutions_directory = solutions_directory / year _ask_user_to_mkdir(year_solutions_directory, f'{year} solution files') + _create_files(year_solutions_directory, day) def _abort_if_puzzle_locked(year: int, day: int): From 0febc18726ee07ba14fb446dd7cf900e92243a3f Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 19:36:22 +0200 Subject: [PATCH 105/176] Revert "Add Jinja to Python packages requirements file" This reverts commit 0e618b8a7b8b47fd64bb12248b05fd60957b6c34. --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index fa7b4464..fa3b6c57 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ requests click -pyyaml -jinja2 \ No newline at end of file +pyyaml \ No newline at end of file From 553cd4f62d2d7a866175a81b435eba7533aed920 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 19:39:31 +0200 Subject: [PATCH 106/176] Create commands utilities file --- solution_runner/commands/commands_utils.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 solution_runner/commands/commands_utils.py diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py new file mode 100644 index 00000000..e69de29b From d6910b601808d6cb04f170b12113a6ef9203c146 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 22:27:16 +0200 Subject: [PATCH 107/176] Move setting getter function into commands utils to simplify imports --- solution_runner/commands/commands_utils.py | 21 +++++++++++++++++++++ solution_runner/commands/setup_command.py | 7 +++---- solution_runner/utils.py | 20 -------------------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index e69de29b..c6877fdb 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -0,0 +1,21 @@ +from typing import Any + +import click +import yaml + +import consts + + +def get_setting(key: str) -> Any: + """ + :param key: key to retrieve its value + :return: value of `key` which is stored in the configuration file + """ + configuration_file = consts.APP_DATA_DIRECTORY / consts.CONFIGURATION_FILE_NAME + try: + configuration = yaml.safe_load(configuration_file.read_text()) + except FileNotFoundError: + click.secho("configuration file doesn't exist. Run config command first", fg='red') + raise + + return configuration[key] diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 95119ff1..f3b2cb55 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -4,10 +4,9 @@ import click import requests -from . import consts +from . import consts, commands_utils from .consts import Directories, FileExtensions from .defaults_and_choices import get_default_year -from .. import utils _default_year = get_default_year() @@ -27,7 +26,7 @@ def command(year: int, day: int, should_use_cache: bool): day = str(day).zfill(2) # Add a leading zero for single digit numbers. try: - root_directory = utils.get_setting(consts.ROOT_DIRECTORY) + root_directory = commands_utils.get_setting(consts.ROOT_DIRECTORY) except FileNotFoundError: click.Context(command).abort() inputs_directory = root_directory / Directories.INPUTS @@ -42,7 +41,7 @@ def command(year: int, day: int, should_use_cache: bool): input_file.touch() try: - session_id = utils.get_setting(consts.SESSION_ID) + session_id = commands_utils.get_setting(consts.SESSION_ID) except FileNotFoundError: click.Context(command).abort() diff --git a/solution_runner/utils.py b/solution_runner/utils.py index 50373bf4..99453c1f 100644 --- a/solution_runner/utils.py +++ b/solution_runner/utils.py @@ -1,12 +1,7 @@ import importlib from pathlib import Path from types import ModuleType -from typing import Any -import click -import yaml - -import commands.consts import consts @@ -20,18 +15,3 @@ def get_module_from_filepath(filepath: Path, directory_relative_to: Path | str = for path_form, module_form in consts.SEPARATORS_TO_MODULE_NOTATION.items(): module_name = module_name.replace(path_form, module_form) return importlib.import_module(module_name) - - -def get_setting(key: str) -> Any: - """ - :param key: key to retrieve its value - :return: value of `key` which is stored in the configuration file - """ - configuration_file = commands.consts.APP_DATA_DIRECTORY / commands.consts.CONFIGURATION_FILE_NAME - try: - configuration = yaml.safe_load(configuration_file.read_text()) - except FileNotFoundError: - click.secho("configuration file doesn't exist. Run config command first", fg='red') - raise - - return configuration[key] From 92a3b40591008547cd3a1e826b87200c6463523b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 22:52:32 +0200 Subject: [PATCH 108/176] Delete unused `COOKIE` constant --- solution_runner/commands/consts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index c865c7b4..bae8dcc5 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -8,7 +8,6 @@ FIRST_AOC_YEAR = 2015 ZERO = '0' INPUT_URL = 'https://adventofcode.com/{year}/day/{day}/input' -COOKIE = {'session': None} APP_DATA_DIRECTORY = click.get_app_dir('Advent of Code') CONFIGURATION_FILE_NAME = Path('configuration.yaml') From d6fb354b8235b2390c2f2dd5b82ea71b7a6faf8b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 22:55:22 +0200 Subject: [PATCH 109/176] Extract "session" into constant --- solution_runner/commands/consts.py | 1 + solution_runner/commands/setup_command.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index bae8dcc5..80dbc858 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -8,6 +8,7 @@ FIRST_AOC_YEAR = 2015 ZERO = '0' INPUT_URL = 'https://adventofcode.com/{year}/day/{day}/input' +SESSION = 'session' APP_DATA_DIRECTORY = click.get_app_dir('Advent of Code') CONFIGURATION_FILE_NAME = Path('configuration.yaml') diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index f3b2cb55..34adc29a 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -120,7 +120,7 @@ def _download_input(year: str, day: str, input_file: Path, session_id: str): """ day = day.lstrip(consts.ZERO) url = consts.INPUT_URL.format(year=year, day=day) - cookie = {'session': session_id} + cookie = {consts.SESSION: session_id} request = requests.get(url, cookies=cookie) request.raise_for_status() From 8a15463b9893f6c4643a3e82b6b41b3655992f74 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 23:00:23 +0200 Subject: [PATCH 110/176] Use `__file__` in solution file constant to convert to absolute path --- solution_runner/commands/consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 80dbc858..16b845ca 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -15,7 +15,7 @@ ROOT_DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True) ROOT_DIRECTORY = 'root directory' SESSION_ID = 'session ID' -SOLUTION_FILE_CONTENT = Path('solution_template.py').read_text() +SOLUTION_FILE_CONTENT = Path(Path(__file__).parent, 'solution_template.py').read_text() class Directories: From 078e4496ffa1e8c35984d2c1918c17f467e06077 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 23:05:24 +0200 Subject: [PATCH 111/176] Extract December month number into a constant --- solution_runner/commands/consts.py | 1 + solution_runner/commands/defaults_and_choices.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 16b845ca..4cfe7a7f 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -6,6 +6,7 @@ FIRST_AOC_YEAR = 2015 +DECEMBER = 12 ZERO = '0' INPUT_URL = 'https://adventofcode.com/{year}/day/{day}/input' SESSION = 'session' diff --git a/solution_runner/commands/defaults_and_choices.py b/solution_runner/commands/defaults_and_choices.py index 4ac88de7..07b88721 100644 --- a/solution_runner/commands/defaults_and_choices.py +++ b/solution_runner/commands/defaults_and_choices.py @@ -1,5 +1,7 @@ from datetime import datetime +import consts + def get_default_year() -> int: """ @@ -7,7 +9,7 @@ def get_default_year() -> int: """ today = datetime.today() current_year = today.year - if today.month == 12: + if today.month == consts.DECEMBER: return current_year else: return current_year - 1 From 3bd174de9f0552df356aeae1e86ae6172429dd23 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 23:14:34 +0200 Subject: [PATCH 112/176] Extract Advent days range into a constant --- solution_runner/commands/consts.py | 1 + solution_runner/commands/setup_command.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 4cfe7a7f..e5464e48 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -7,6 +7,7 @@ FIRST_AOC_YEAR = 2015 DECEMBER = 12 +ADVENT_DAYS_RANGE = click.IntRange(1, 25) ZERO = '0' INPUT_URL = 'https://adventofcode.com/{year}/day/{day}/input' SESSION = 'session' diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 34adc29a..b03f6b49 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -15,7 +15,7 @@ @click.command(name='setup') @click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), default=_default_year, show_default=f'last year: {_default_year}', help='year of puzzle setting up solution for') -@click.option('-d', '--day', type=click.IntRange(1, 25), required=True, help='day of puzzle setting up solution for') +@click.option('-d', '--day', type=consts.ADVENT_DAYS_RANGE, required=True, help='day of puzzle setting up solution for') @click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, show_default='true', help='whether to use cached input file') def command(year: int, day: int, should_use_cache: bool): From a141d06723c92050fe4e0d3313b252a6e6d82b79 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 23 Jan 2022 23:18:42 +0200 Subject: [PATCH 113/176] Extract solution parts into a constant --- solution_runner/commands/consts.py | 1 + solution_runner/commands/setup_command.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index e5464e48..3aabf9b2 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -16,6 +16,7 @@ CONFIGURATION_FILE_NAME = Path('configuration.yaml') ROOT_DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True) ROOT_DIRECTORY = 'root directory' +SOLUTION_PARTS = ('1', '2') SESSION_ID = 'session ID' SOLUTION_FILE_CONTENT = Path(Path(__file__).parent, 'solution_template.py').read_text() diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index b03f6b49..b4d20c78 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -105,7 +105,7 @@ def _create_files(year_solutions_directory: Path, day: str): """ solutions_directory = year_solutions_directory / day solutions_directory.mkdir() - for part in ('1', '2'): + for part in consts.SOLUTION_PARTS: filepath = (solutions_directory / part).with_suffix(FileExtensions.PYTHON) filepath.write_text(consts.SOLUTION_FILE_CONTENT) From 7b1aba35d0dfe20eeeda6c55592a678dbd42f2ef Mon Sep 17 00:00:00 2001 From: Dan Katzuv <31829093+katzuv@users.noreply.github.com> Date: Thu, 3 Feb 2022 22:00:55 +0200 Subject: [PATCH 114/176] Use `Iterable` instead of `Iterator` in type hint Co-authored-by: Yoni Katzuv --- solution_runner/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index 3b522ea8..c30c11c1 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -18,7 +18,7 @@ def cli() -> click.Group: """ -def _add_commands_to_cli(cli: click.Group, commands_files_paths: Iterator[Path]) -> click.Group: +def _add_commands_to_cli(cli: click.Group, commands_files_paths: Iterable[Path]) -> click.Group: """ :param cli: click.Group object :param commands_files_paths: path to directory containing Python files with Python subcommands From 0ac571ef8032bbea1577127c227a759acc71618d Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 11 Feb 2022 19:59:13 +0200 Subject: [PATCH 115/176] Remove unneeded `else` --- solution_runner/commands/defaults_and_choices.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/solution_runner/commands/defaults_and_choices.py b/solution_runner/commands/defaults_and_choices.py index 07b88721..198f3bf8 100644 --- a/solution_runner/commands/defaults_and_choices.py +++ b/solution_runner/commands/defaults_and_choices.py @@ -11,5 +11,4 @@ def get_default_year() -> int: current_year = today.year if today.month == consts.DECEMBER: return current_year - else: - return current_year - 1 + return current_year - 1 From aba958259ff2f5d78c60450241d0b1b8f082ba00 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 11 Feb 2022 20:01:56 +0200 Subject: [PATCH 116/176] Explicitly refer to the current working directory instead of `.` --- solution_runner/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/utils.py b/solution_runner/utils.py index 99453c1f..3bb17db0 100644 --- a/solution_runner/utils.py +++ b/solution_runner/utils.py @@ -5,7 +5,7 @@ import consts -def get_module_from_filepath(filepath: Path, directory_relative_to: Path | str = Path('.')) -> ModuleType: +def get_module_from_filepath(filepath: Path, directory_relative_to: Path | str = Path.cwd()) -> ModuleType: """ :param filepath: file path to import :param directory_relative_to: directory to compute relative path to from `file_path`, default is current directory From 8594d00d0f927c6aeebd92bdf6a141e5c061ae7e Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 11 Feb 2022 20:08:39 +0200 Subject: [PATCH 117/176] Use `os.sep` instead of explicitly declaring separators --- solution_runner/consts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/solution_runner/consts.py b/solution_runner/consts.py index def98e4a..e1ff95a6 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,11 +1,12 @@ +import os from pathlib import Path COMMAND_FILE_SUFFIX = '_command.py' _COMMANDS_DIRECTORY_PATH = Path(__file__).parent / Path('commands') COMMANDS_FILES_PATHS = tuple(_COMMANDS_DIRECTORY_PATH.glob(f'*{COMMAND_FILE_SUFFIX}')) -# Linux and Windows separators to module notation, including upper package and accessing module inside package. -SEPARATORS_TO_MODULE_NOTATION = {'../': '..', '/': '.', '..\\': '..', '\\': '.'} +# Separators to module notation, including upper package and accessing module inside package. +SEPARATORS_TO_MODULE_NOTATION = {f'..{os.sep}': '..', os.sep: '.'} class CliConstants: From f09a1b3133ef4ef67da871078c2f197376816173 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 11 Feb 2022 20:12:56 +0200 Subject: [PATCH 118/176] Cast `directory_relative_to` to `Path` if it is an `str` --- solution_runner/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/solution_runner/utils.py b/solution_runner/utils.py index 3bb17db0..9fc4453e 100644 --- a/solution_runner/utils.py +++ b/solution_runner/utils.py @@ -11,6 +11,8 @@ def get_module_from_filepath(filepath: Path, directory_relative_to: Path | str = :param directory_relative_to: directory to compute relative path to from `file_path`, default is current directory :return: imported module from `file_path` """ + if isinstance(directory_relative_to, str): + directory_relative_to = Path(directory_relative_to) module_name = str(filepath.relative_to(directory_relative_to.absolute()).with_suffix('')) for path_form, module_form in consts.SEPARATORS_TO_MODULE_NOTATION.items(): module_name = module_name.replace(path_form, module_form) From b648ca1544e15529879351413dba360e0a68f8d2 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 16 Oct 2022 20:33:50 +0300 Subject: [PATCH 119/176] Change type annotation from `Iterable` to `Iterator` --- solution_runner/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index c30c11c1..3b522ea8 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -18,7 +18,7 @@ def cli() -> click.Group: """ -def _add_commands_to_cli(cli: click.Group, commands_files_paths: Iterable[Path]) -> click.Group: +def _add_commands_to_cli(cli: click.Group, commands_files_paths: Iterator[Path]) -> click.Group: """ :param cli: click.Group object :param commands_files_paths: path to directory containing Python files with Python subcommands From 167a9226fff4876fd237494e8e2dad12fc038fe2 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 16 Oct 2022 20:40:13 +0300 Subject: [PATCH 120/176] Remove dynamic commands loading code Too complex and not worth it for a single digit number of commands. --- solution_runner/cli.py | 24 +----------------------- solution_runner/consts.py | 11 ----------- solution_runner/utils.py | 19 ------------------- 3 files changed, 1 insertion(+), 53 deletions(-) delete mode 100644 solution_runner/utils.py diff --git a/solution_runner/cli.py b/solution_runner/cli.py index 3b522ea8..eaedab57 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -1,12 +1,6 @@ -import os -from pathlib import Path -from typing import Iterator - import click -import consts -from consts import CliConstants -from utils import get_module_from_filepath +from solution_runner.consts import CliConstants @click.group(context_settings=CliConstants.CONTEXT) @@ -18,24 +12,8 @@ def cli() -> click.Group: """ -def _add_commands_to_cli(cli: click.Group, commands_files_paths: Iterator[Path]) -> click.Group: - """ - :param cli: click.Group object - :param commands_files_paths: path to directory containing Python files with Python subcommands - :return: CLI with added subcommands - """ - file_directory = Path(__file__).parent - current_directory = Path.cwd() - os.chdir(file_directory) - for filepath in commands_files_paths: - module = get_module_from_filepath(filepath) - cli.add_command(module.command) - os.chdir(current_directory) - - def main(): """Add subcommands to the solution runner CLI and run it.""" - _add_commands_to_cli(cli, consts.COMMANDS_FILES_PATHS) cli() diff --git a/solution_runner/consts.py b/solution_runner/consts.py index e1ff95a6..11a7fb31 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,13 +1,2 @@ -import os -from pathlib import Path - - -COMMAND_FILE_SUFFIX = '_command.py' -_COMMANDS_DIRECTORY_PATH = Path(__file__).parent / Path('commands') -COMMANDS_FILES_PATHS = tuple(_COMMANDS_DIRECTORY_PATH.glob(f'*{COMMAND_FILE_SUFFIX}')) -# Separators to module notation, including upper package and accessing module inside package. -SEPARATORS_TO_MODULE_NOTATION = {f'..{os.sep}': '..', os.sep: '.'} - - class CliConstants: CONTEXT = {'help_option_names': ['-h', '--help']} diff --git a/solution_runner/utils.py b/solution_runner/utils.py deleted file mode 100644 index 9fc4453e..00000000 --- a/solution_runner/utils.py +++ /dev/null @@ -1,19 +0,0 @@ -import importlib -from pathlib import Path -from types import ModuleType - -import consts - - -def get_module_from_filepath(filepath: Path, directory_relative_to: Path | str = Path.cwd()) -> ModuleType: - """ - :param filepath: file path to import - :param directory_relative_to: directory to compute relative path to from `file_path`, default is current directory - :return: imported module from `file_path` - """ - if isinstance(directory_relative_to, str): - directory_relative_to = Path(directory_relative_to) - module_name = str(filepath.relative_to(directory_relative_to.absolute()).with_suffix('')) - for path_form, module_form in consts.SEPARATORS_TO_MODULE_NOTATION.items(): - module_name = module_name.replace(path_form, module_form) - return importlib.import_module(module_name) From f2769ac89f7ef2216aa66625e6bd0b41e457a3ca Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 16 Oct 2022 22:35:04 +0300 Subject: [PATCH 121/176] Use right "default year" function --- solution_runner/input_getter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/input_getter.py b/solution_runner/input_getter.py index b389c0d4..66c17254 100644 --- a/solution_runner/input_getter.py +++ b/solution_runner/input_getter.py @@ -3,6 +3,7 @@ import requests from session_id import SESSION_ID +from solution_runner.commands.defaults_and_choices import get_default_year URL = 'https://adventofcode.com/{year}/day/{day_number}/input' @@ -23,7 +24,7 @@ def get_input(year: str, day_number: str) -> str: def get_year_input() -> str: - current_year = _get_default_year() + current_year = get_default_year() year_input = input(f'Enter year ({current_year} is default): ') if year_input == '': year_input = current_year From 845ffecb799b714177a2d53920751c33a9bb678a Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 16 Oct 2022 23:40:22 +0300 Subject: [PATCH 122/176] Rearrange `import` statements --- solution_runner/cli.py | 2 +- solution_runner/commands/commands_utils.py | 2 +- solution_runner/commands/consts.py | 2 +- solution_runner/commands/defaults_and_choices.py | 2 +- solution_runner/input_getter.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index eaedab57..81b9ee07 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -1,6 +1,6 @@ import click -from solution_runner.consts import CliConstants +from consts import CliConstants @click.group(context_settings=CliConstants.CONTEXT) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index c6877fdb..2db7eff8 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -3,7 +3,7 @@ import click import yaml -import consts +from . import consts def get_setting(key: str) -> Any: diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 3aabf9b2..ae3fdf68 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -9,7 +9,7 @@ DECEMBER = 12 ADVENT_DAYS_RANGE = click.IntRange(1, 25) ZERO = '0' -INPUT_URL = 'https://adventofcode.com/{year}/day/{day}/input' +INPUT_URL = 'https://adventofcode.com/$year/day/$day/input' SESSION = 'session' APP_DATA_DIRECTORY = click.get_app_dir('Advent of Code') diff --git a/solution_runner/commands/defaults_and_choices.py b/solution_runner/commands/defaults_and_choices.py index 198f3bf8..ece55fa3 100644 --- a/solution_runner/commands/defaults_and_choices.py +++ b/solution_runner/commands/defaults_and_choices.py @@ -1,6 +1,6 @@ from datetime import datetime -import consts +from . import consts def get_default_year() -> int: diff --git a/solution_runner/input_getter.py b/solution_runner/input_getter.py index 66c17254..730fef83 100644 --- a/solution_runner/input_getter.py +++ b/solution_runner/input_getter.py @@ -2,8 +2,8 @@ import requests +from commands.defaults_and_choices import get_default_year from session_id import SESSION_ID -from solution_runner.commands.defaults_and_choices import get_default_year URL = 'https://adventofcode.com/{year}/day/{day_number}/input' From 14a571ad24f31451476513e6eab1327f168259c0 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 17 Oct 2022 01:31:12 +0300 Subject: [PATCH 123/176] Remove `main` function that contains only a single line --- solution_runner/cli.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index 81b9ee07..d9a3798a 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -12,10 +12,5 @@ def cli() -> click.Group: """ -def main(): - """Add subcommands to the solution runner CLI and run it.""" - cli() - - if __name__ == '__main__': - main() + cli() From 0845ed5d3592a554d8ceed4ed2f1f18f90f33164 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 17 Oct 2022 01:35:46 +0300 Subject: [PATCH 124/176] Replace `string.format` with `string.Template` for solution folder path --- solution_runner/input_getter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/input_getter.py b/solution_runner/input_getter.py index 730fef83..10076fec 100644 --- a/solution_runner/input_getter.py +++ b/solution_runner/input_getter.py @@ -13,7 +13,7 @@ INPUT_DIRECTORY_NAME = Path('inputs') TXT_SUFFIX = '.txt' -DAY_DIRECTORY_PATH = 'd{day_number}' +DAY_DIRECTORY_PATH = 'd$day_number' def get_input(year: str, day_number: str) -> str: From 1848beb1e16ffecad3aa755b9ce7a216ed3abfc9 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 17 Oct 2022 01:45:13 +0300 Subject: [PATCH 125/176] Create submit command --- solution_runner/commands/submit_command.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 solution_runner/commands/submit_command.py diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py new file mode 100644 index 00000000..8d125c05 --- /dev/null +++ b/solution_runner/commands/submit_command.py @@ -0,0 +1,7 @@ +import click + + +@click.command(name='submit') +def command(): + """Submit solution.""" + pass From 5d902ddf7948ea395472428c4bbb9ad9b904523c Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 17 Oct 2022 02:20:55 +0300 Subject: [PATCH 126/176] Replace `string.format` with `string.Template` for input URL --- solution_runner/commands/consts.py | 3 ++- solution_runner/commands/setup_command.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index ae3fdf68..30507bc0 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -1,3 +1,4 @@ +import string from datetime import datetime from pathlib import Path from zoneinfo import ZoneInfo @@ -9,7 +10,7 @@ DECEMBER = 12 ADVENT_DAYS_RANGE = click.IntRange(1, 25) ZERO = '0' -INPUT_URL = 'https://adventofcode.com/$year/day/$day/input' +INPUT_URL = string.Template('https://adventofcode.com/$year/day/$day/input') SESSION = 'session' APP_DATA_DIRECTORY = click.get_app_dir('Advent of Code') diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index b4d20c78..87a25725 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -119,7 +119,7 @@ def _download_input(year: str, day: str, input_file: Path, session_id: str): :param session_id: session ID to download input from website """ day = day.lstrip(consts.ZERO) - url = consts.INPUT_URL.format(year=year, day=day) + url = consts.INPUT_URL.substitute(year=year, day=day) cookie = {consts.SESSION: session_id} request = requests.get(url, cookies=cookie) From 06f01075e51b7795bc4775344d676f0e8646fa84 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 17 Oct 2022 02:21:53 +0300 Subject: [PATCH 127/176] Remove obsolete "input getter" module --- solution_runner/input_getter.py | 69 --------------------------------- 1 file changed, 69 deletions(-) delete mode 100644 solution_runner/input_getter.py diff --git a/solution_runner/input_getter.py b/solution_runner/input_getter.py deleted file mode 100644 index 10076fec..00000000 --- a/solution_runner/input_getter.py +++ /dev/null @@ -1,69 +0,0 @@ -from pathlib import Path - -import requests - -from commands.defaults_and_choices import get_default_year -from session_id import SESSION_ID - - -URL = 'https://adventofcode.com/{year}/day/{day_number}/input' -COOKIE = {'session': SESSION_ID} -HTTP_OK_CODE = 200 - -INPUT_DIRECTORY_NAME = Path('inputs') -TXT_SUFFIX = '.txt' - -DAY_DIRECTORY_PATH = 'd$day_number' - - -def get_input(year: str, day_number: str) -> str: - url = URL.format(year=year, day_number=day_number) - request = requests.get(url, cookies=COOKIE) - request.raise_for_status() - return request.text - - -def get_year_input() -> str: - current_year = get_default_year() - year_input = input(f'Enter year ({current_year} is default): ') - if year_input == '': - year_input = current_year - return year_input - - -def get_day_input() -> str: - day_number_input = input('Enter day number: ') - return day_number_input - - -def write_input_file(year: str, day: str, input_text: str): - input_directory_path = Path(year, INPUT_DIRECTORY_NAME) - input_directory_path.mkdir(parents=True, exist_ok=True) - input_file = (input_directory_path / day).with_suffix(TXT_SUFFIX) - input_file.write_text(input_text) - - -def create_solutions_directory(year: str, day: str): - padded_day_number = day.rjust(2, '0') - solution_directory_path = Path(year, DAY_DIRECTORY_PATH.format(day_number=padded_day_number)) - if solution_directory_path.exists(): - return - - solution_directory_path.mkdir() - for part in (1, 2): - part_solution_path = solution_directory_path / f'p{part}.py' - part_solution_path.touch() - - -def main(): - year_input = get_year_input() - day_input = get_day_input() - - input_text = get_input(year_input, day_input) - write_input_file(year_input, day_input, input_text) - - create_solutions_directory(year_input, day_input) - - -if __name__ == '__main__': - main() From fb39ea31df6cbb96617112768496318054e1d67c Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 17 Oct 2022 12:31:59 +0300 Subject: [PATCH 128/176] Prepend "p" before solution part name --- solution_runner/commands/consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 30507bc0..d399a3b7 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -17,7 +17,7 @@ CONFIGURATION_FILE_NAME = Path('configuration.yaml') ROOT_DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True) ROOT_DIRECTORY = 'root directory' -SOLUTION_PARTS = ('1', '2') +SOLUTION_PARTS = ('p1', 'p2') SESSION_ID = 'session ID' SOLUTION_FILE_CONTENT = Path(Path(__file__).parent, 'solution_template.py').read_text() From 996e43b871a6f0021e012ae34cef6ba6d501e545 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 17 Oct 2022 16:08:38 +0300 Subject: [PATCH 129/176] Replace underscore with hyphen in `setup` command option --- solution_runner/commands/setup_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 87a25725..04e51ca7 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -16,7 +16,7 @@ @click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), default=_default_year, show_default=f'last year: {_default_year}', help='year of puzzle setting up solution for') @click.option('-d', '--day', type=consts.ADVENT_DAYS_RANGE, required=True, help='day of puzzle setting up solution for') -@click.option('--use_cache/--ignore_cache', 'should_use_cache', default=True, show_default='true', +@click.option('--use-cache/--ignore-cache', 'should_use_cache', default=True, show_default='true', help='whether to use cached input file') def command(year: int, day: int, should_use_cache: bool): """Set up a solution: fetch input and create solution files.""" From e6e70f67ff2d71685c937727fecdc653e71130b1 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 17 Oct 2022 16:16:38 +0300 Subject: [PATCH 130/176] Prepend puzzle solution directory with `d` `d` for "day". --- solution_runner/commands/setup_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 04e51ca7..18ebf50d 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -103,7 +103,7 @@ def _create_files(year_solutions_directory: Path, day: str): :param year_solutions_directory: challenges solution files of the relevant year :param day: day of the challenge """ - solutions_directory = year_solutions_directory / day + solutions_directory = year_solutions_directory / f"d{day}" solutions_directory.mkdir() for part in consts.SOLUTION_PARTS: filepath = (solutions_directory / part).with_suffix(FileExtensions.PYTHON) From fec4225b7a0b28c2e1a14e50431c3f036d2cc4f2 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Tue, 18 Oct 2022 00:06:42 +0300 Subject: [PATCH 131/176] Add `PathType` enum --- solution_runner/commands/commands_utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index 2db7eff8..257708e1 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Any import click @@ -6,6 +7,11 @@ from . import consts +class PathType(Enum): + FILE = 1 + DIRECTORY = 2 + + def get_setting(key: str) -> Any: """ :param key: key to retrieve its value From 186525878ae4d4cccbd1c4d50c2800008e8753d4 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Tue, 18 Oct 2022 00:09:52 +0300 Subject: [PATCH 132/176] Add function that checks whether a path exists and aborts if it does not --- solution_runner/commands/commands_utils.py | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index 257708e1..bacaa069 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -1,4 +1,5 @@ from enum import Enum +from pathlib import Path from typing import Any import click @@ -25,3 +26,25 @@ def get_setting(key: str) -> Any: raise return configuration[key] + + +def check_path_exists(path: Path, command: click.Command, path_type: str = None): + """ + Abort if path doesn't exist. + :param path: path to check + :param command: command to be passed to`click.Context` if aborting is needed + :param path_type: optional "dir" or "file". If not passed, only check for existence. If passed, check for type. + """ + match path_type: + case None: + if not path.exists(): + click.secho(f"{path} doesn't exist", fg='red') + case "dir": + if not path.is_dir(): + click.secho(f"Directory at {path} doesn't exist or isn't a directory", fg='red') + case "file": + if not path.is_file(): + click.secho(f"File at {path} doesn't exist or isn't a file", fg='red') + case _: + return # If everything is OK, exit the function before aborting. + click.Context(command).abort() From ced68d4276f56562744e62463f25eaa94b2299f6 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 30 Oct 2022 22:46:25 +0200 Subject: [PATCH 133/176] Add year option to `submit` command --- solution_runner/commands/submit_command.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 8d125c05..7f404184 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -1,7 +1,15 @@ import click +from . import consts +from .defaults_and_choices import get_default_year + + +_default_year = get_default_year() + @click.command(name='submit') -def command(): +@click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), default=_default_year, + show_default=f'last year: {_default_year}', help='year of puzzle setting up solution for') +def command(year: int): """Submit solution.""" pass From 22562d6846dde73be32c720e56d0a6199f802838 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 30 Oct 2022 22:47:48 +0200 Subject: [PATCH 134/176] Add day option to submit command --- solution_runner/commands/submit_command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 7f404184..5ee2c222 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -10,6 +10,7 @@ @click.command(name='submit') @click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), default=_default_year, show_default=f'last year: {_default_year}', help='year of puzzle setting up solution for') -def command(year: int): +@click.option('-d', '--day', type=consts.ADVENT_DAYS_RANGE, required=True, help='day of puzzle to submit') +def command(year: int, day: int): """Submit solution.""" pass From 2cecc694d267b16ff4cb76fe04f6c2757cb1ac89 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 30 Oct 2022 22:48:25 +0200 Subject: [PATCH 135/176] Add part option to `submit` command --- solution_runner/commands/submit_command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 5ee2c222..1e8570ba 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -11,6 +11,7 @@ @click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), default=_default_year, show_default=f'last year: {_default_year}', help='year of puzzle setting up solution for') @click.option('-d', '--day', type=consts.ADVENT_DAYS_RANGE, required=True, help='day of puzzle to submit') -def command(year: int, day: int): +@click.option('-p', '--part', type=click.Choice(('1', '2')), required=True, help='puzzle part to submit') +def command(year: int, day: int, part: int): """Submit solution.""" pass From 3ab890d15395bcec21a3c2714bfc17b4413bce94 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 30 Oct 2022 22:49:06 +0200 Subject: [PATCH 136/176] Convert year and day to `str`s --- solution_runner/commands/submit_command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 1e8570ba..6d7d661c 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -14,4 +14,5 @@ @click.option('-p', '--part', type=click.Choice(('1', '2')), required=True, help='puzzle part to submit') def command(year: int, day: int, part: int): """Submit solution.""" - pass + year = str(year) + day = str(day) \ No newline at end of file From 36046b1e278d5719cd2f3296daadc1cd04da911b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 30 Oct 2022 22:52:01 +0200 Subject: [PATCH 137/176] Get puzzle solutions directory and abort if it doesn't exist --- solution_runner/commands/submit_command.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 6d7d661c..0f77947f 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -1,6 +1,8 @@ import click from . import consts +from .commands_utils import get_setting +from .consts import Directories from .defaults_and_choices import get_default_year @@ -15,4 +17,10 @@ def command(year: int, day: int, part: int): """Submit solution.""" year = str(year) - day = str(day) \ No newline at end of file + day = str(day) + + root_directory = get_setting(consts.ROOT_DIRECTORY) + solutions_directory = root_directory / Directories.SOLUTIONS / year / f"d{day}" + if not solutions_directory.is_dir(): + click.secho("Solution directory does not exist. Run setup command first", fg='red') + click.Context(command).abort() \ No newline at end of file From 01eb7e6c2d4e6c587497111c578bfd01fc86f897 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 30 Oct 2022 22:53:34 +0200 Subject: [PATCH 138/176] Get puzzle input --- solution_runner/commands/submit_command.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 0f77947f..4206a7ec 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -2,7 +2,7 @@ from . import consts from .commands_utils import get_setting -from .consts import Directories +from .consts import Directories, FileExtensions from .defaults_and_choices import get_default_year @@ -23,4 +23,7 @@ def command(year: int, day: int, part: int): solutions_directory = root_directory / Directories.SOLUTIONS / year / f"d{day}" if not solutions_directory.is_dir(): click.secho("Solution directory does not exist. Run setup command first", fg='red') - click.Context(command).abort() \ No newline at end of file + click.Context(command).abort() + + input_path = (root_directory / Directories.INPUTS / year / day).with_suffix(FileExtensions.TEXT) + input_text = input_path.read_text() From 426ca77ff66484fe85ba6c8a43eb72ff6a3ba4b2 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Sun, 13 Nov 2022 08:39:35 +0200 Subject: [PATCH 139/176] Get puzzle solution and if error has occurred print it and abort --- solution_runner/commands/submit_command.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 4206a7ec..e524c45a 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -1,3 +1,5 @@ +import subprocess + import click from . import consts @@ -27,3 +29,14 @@ def command(year: int, day: int, part: int): input_path = (root_directory / Directories.INPUTS / year / day).with_suffix(FileExtensions.TEXT) input_text = input_path.read_text() + + part_solution_path = (solutions_directory / f"p{part}").with_suffix(FileExtensions.PYTHON) + command_arguments = ("python", part_solution_path, input_text) + try: + result = subprocess.run(command_arguments, capture_output=True, check=True, text=True) + # If an error occurred in the called solution file, print the exception and abort. + except subprocess.CalledProcessError as error: + print(error.stderr) + raise click.Abort() + + solution = result.stdout.strip() From 308d1fdb3fd02e072b4251ba102f85bc202f9466 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 14 Nov 2022 20:00:59 +0200 Subject: [PATCH 140/176] Add constant for base website URL --- solution_runner/commands/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index d399a3b7..4cb29204 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -10,6 +10,7 @@ DECEMBER = 12 ADVENT_DAYS_RANGE = click.IntRange(1, 25) ZERO = '0' +BASE_URL = 'https://adventofcode.com/' INPUT_URL = string.Template('https://adventofcode.com/$year/day/$day/input') SESSION = 'session' From 0cda252b51ca898bfc46c06d3d871af9de48e417 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 14 Nov 2022 20:01:52 +0200 Subject: [PATCH 141/176] Add utility function that sends requests to the Advent of Code website --- solution_runner/commands/commands_utils.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index bacaa069..338cc6f8 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -1,8 +1,10 @@ +import urllib.parse from enum import Enum from pathlib import Path from typing import Any import click +import requests import yaml from . import consts @@ -48,3 +50,22 @@ def check_path_exists(path: Path, command: click.Command, path_type: str = None) case _: return # If everything is OK, exit the function before aborting. click.Context(command).abort() + + +def send_aoc_request(method, endpoint: str, payload=None) -> str: + """ + Send a request to Advent of Code's website and return the textual response. + :param method: method of the request + :param endpoint: endpoint to append to the base URL + :param payload: optional payload to attach to the request + :return: text content of the response + """ + if payload is None: + payload = {} + session_id = get_setting(consts.SESSION_ID) + cookies = {consts.SESSION: session_id} + url = urllib.parse.urljoin(consts.BASE_URL, endpoint) + + request = requests.request(method, url, cookies=cookies, data=payload) + request.raise_for_status() + return request.text From 040de2fa5f946786362708ca5f9e856f3ef97144 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 14 Nov 2022 20:04:34 +0200 Subject: [PATCH 142/176] Add class for HTTP request methods --- solution_runner/commands/consts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 4cb29204..b3777064 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -33,5 +33,10 @@ class FileExtensions: PYTHON = '.py' +class HttpMethods: + GET = 'GET' + POST = 'POST' + + US_EASTERN_TIMEZONE = ZoneInfo('US/Eastern') AOC_UNLOCK_TIME_TEMPLATE = datetime(year=1, month=12, day=1, hour=0, tzinfo=US_EASTERN_TIMEZONE) From 482835342647dc784049eea41c65ae2dbdd7404b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 14 Nov 2022 20:07:42 +0200 Subject: [PATCH 143/176] Use new HTTP request function --- solution_runner/commands/consts.py | 2 +- solution_runner/commands/setup_command.py | 12 +++--------- solution_runner/commands/submit_command.py | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index b3777064..876eec48 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -11,7 +11,7 @@ ADVENT_DAYS_RANGE = click.IntRange(1, 25) ZERO = '0' BASE_URL = 'https://adventofcode.com/' -INPUT_URL = string.Template('https://adventofcode.com/$year/day/$day/input') +INPUT_ENDPOINT_TEMPLATE = string.Template('/$year/day/$day/input') SESSION = 'session' APP_DATA_DIRECTORY = click.get_app_dir('Advent of Code') diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 18ebf50d..c98da465 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -2,10 +2,9 @@ from pathlib import Path import click -import requests from . import consts, commands_utils -from .consts import Directories, FileExtensions +from .consts import Directories, FileExtensions, HttpMethods from .defaults_and_choices import get_default_year @@ -119,11 +118,6 @@ def _download_input(year: str, day: str, input_file: Path, session_id: str): :param session_id: session ID to download input from website """ day = day.lstrip(consts.ZERO) - url = consts.INPUT_URL.substitute(year=year, day=day) - cookie = {consts.SESSION: session_id} - - request = requests.get(url, cookies=cookie) - request.raise_for_status() - - input_text = request.text + endpoint = consts.INPUT_ENDPOINT_TEMPLATE.substitute(year=year, day=day) + input_text = commands_utils.send_aoc_request(HttpMethods.GET, endpoint) input_file.write_text(input_text) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index e524c45a..74130e43 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -4,7 +4,7 @@ from . import consts from .commands_utils import get_setting -from .consts import Directories, FileExtensions +from .consts import Directories, FileExtensions, HttpMethods from .defaults_and_choices import get_default_year From e915df226714f2765dd332c04dec719f0c7c8f48 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 14 Nov 2022 23:21:04 +0200 Subject: [PATCH 144/176] Add custom `User-Agent` header to Advent of Code HTTP requests --- solution_runner/commands/commands_utils.py | 2 +- solution_runner/commands/consts.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index 338cc6f8..2b5dbc5c 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -66,6 +66,6 @@ def send_aoc_request(method, endpoint: str, payload=None) -> str: cookies = {consts.SESSION: session_id} url = urllib.parse.urljoin(consts.BASE_URL, endpoint) - request = requests.request(method, url, cookies=cookies, data=payload) + request = requests.request(method, url, headers=consts.USER_AGENT_HEADER, cookies=cookies, data=payload) request.raise_for_status() return request.text diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 876eec48..9092b19f 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -13,6 +13,8 @@ BASE_URL = 'https://adventofcode.com/' INPUT_ENDPOINT_TEMPLATE = string.Template('/$year/day/$day/input') SESSION = 'session' +# Requested by Advent of Code owner to help track requests. +USER_AGENT_HEADER = {'User-Agent': 'AoC.CLI.katzuv'} APP_DATA_DIRECTORY = click.get_app_dir('Advent of Code') CONFIGURATION_FILE_NAME = Path('configuration.yaml') From 530168a0894e9f878fb43a99aa061258a16f5779 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Tue, 15 Nov 2022 00:11:22 +0200 Subject: [PATCH 145/176] Add Beautiful Soup to Python packages requirements file --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fa3b6c57..db97b425 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ requests click -pyyaml \ No newline at end of file +pyyaml +beautifulsoup4 \ No newline at end of file From 177435132932a9ee04fbaf7536fe049ec997857e Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Tue, 15 Nov 2022 00:17:35 +0200 Subject: [PATCH 146/176] Add submit endpoint template --- solution_runner/commands/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 9092b19f..73cda19f 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -12,6 +12,7 @@ ZERO = '0' BASE_URL = 'https://adventofcode.com/' INPUT_ENDPOINT_TEMPLATE = string.Template('/$year/day/$day/input') +SUBMIT_ENDPOINT_TEMPLATE = string.Template('/$year/day/$day/answer') SESSION = 'session' # Requested by Advent of Code owner to help track requests. USER_AGENT_HEADER = {'User-Agent': 'AoC.CLI.katzuv'} From 6ea814ba2d738c526b049da423ea669f0a79e2e3 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Tue, 15 Nov 2022 00:28:27 +0200 Subject: [PATCH 147/176] Add result parsing function --- solution_runner/commands/submit_command.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 74130e43..898c0a68 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -1,5 +1,6 @@ import subprocess +import bs4 import click from . import consts @@ -11,6 +12,19 @@ _default_year = get_default_year() +def _get_result(result: str) -> tuple[str, bool]: + """ + :param result: result page after submitting an answer + :return: tuple of (result description, whether the solution was correct or not) + """ + parsed_result = bs4.BeautifulSoup(result, 'html.parser') + result_paragraph = parsed_result.find('p').text + first_sentence = result_paragraph.split('.', maxsplit=1)[0] + '.' + + right_answer = "That's the right answer!" in first_sentence + return first_sentence, right_answer + + @click.command(name='submit') @click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), default=_default_year, show_default=f'last year: {_default_year}', help='year of puzzle setting up solution for') From b3bc5555c29bde3a7ad9eb18290d094862dea4e0 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 18:25:03 +0200 Subject: [PATCH 148/176] Submit solution and get result --- solution_runner/commands/submit_command.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 898c0a68..161f9f89 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -3,7 +3,7 @@ import bs4 import click -from . import consts +from . import consts, commands_utils from .commands_utils import get_setting from .consts import Directories, FileExtensions, HttpMethods from .defaults_and_choices import get_default_year @@ -54,3 +54,6 @@ def command(year: int, day: int, part: int): raise click.Abort() solution = result.stdout.strip() + body = {"level": str(part), "answer": solution} + submit_endpoint = consts.SUBMIT_ENDPOINT_TEMPLATE.substitute(year=year, day=day) + result = commands_utils.send_aoc_request(HttpMethods.POST, submit_endpoint, body) From 5f27cf1e7d9f75d16af787f2efbd0a1a939db5b4 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 18:25:41 +0200 Subject: [PATCH 149/176] Print result with matching color --- solution_runner/commands/submit_command.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 161f9f89..c877e3eb 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -57,3 +57,7 @@ def command(year: int, day: int, part: int): body = {"level": str(part), "answer": solution} submit_endpoint = consts.SUBMIT_ENDPOINT_TEMPLATE.substitute(year=year, day=day) result = commands_utils.send_aoc_request(HttpMethods.POST, submit_endpoint, body) + + sentence, is_answer_right = _get_result(result) + color = 'green' if is_answer_right else 'yellow' + click.secho(sentence, fg=color) From 6bdb3e4d6f9017bf867f38fb89703fac55fc3d1b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 18:37:19 +0200 Subject: [PATCH 150/176] Create `__init__.py` in `commands` package --- solution_runner/commands/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 solution_runner/commands/__init__.py diff --git a/solution_runner/commands/__init__.py b/solution_runner/commands/__init__.py new file mode 100644 index 00000000..e69de29b From 45cb47657d006e4691e7587eb74e5148e01d4ba7 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 18:38:05 +0200 Subject: [PATCH 151/176] Add commands to `__init__.py` in `commands` package --- solution_runner/commands/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/solution_runner/commands/__init__.py b/solution_runner/commands/__init__.py index e69de29b..a9ee1b7e 100644 --- a/solution_runner/commands/__init__.py +++ b/solution_runner/commands/__init__.py @@ -0,0 +1,6 @@ +from .config_command import command as config +from .setup_command import command as setup +from .submit_command import command as submit + + +__all__ = ["config", "setup", "submit"] From 8207d979f31a9badfc7418f45d9adc313e778549 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 18:36:43 +0200 Subject: [PATCH 152/176] Add commands to CLI --- solution_runner/cli.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index d9a3798a..b12499c9 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -1,5 +1,6 @@ import click +import commands from consts import CliConstants @@ -13,4 +14,7 @@ def cli() -> click.Group: if __name__ == '__main__': + cli.add_command(commands.setup) + cli.add_command(commands.config) + cli.add_command(commands.submit) cli() From 4f85447d94c34bf07ee79cddc78a240681662386 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 20:27:48 +0200 Subject: [PATCH 153/176] Add Black to Python packages requirements file --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index db97b425..3b77f39f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ requests click pyyaml -beautifulsoup4 \ No newline at end of file +beautifulsoup4 +black \ No newline at end of file From d0ee8ac4fd8144b017cae175cbb8477e1fd1e512 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 20:28:10 +0200 Subject: [PATCH 154/176] Run `black` on whole code --- solution_runner/cli.py | 2 +- solution_runner/commands/commands_utils.py | 16 +++++-- solution_runner/commands/config_command.py | 35 ++++++++++---- solution_runner/commands/consts.py | 46 +++++++++--------- solution_runner/commands/setup_command.py | 54 ++++++++++++++++------ solution_runner/commands/submit_command.py | 52 +++++++++++++++------ solution_runner/consts.py | 2 +- 7 files changed, 142 insertions(+), 65 deletions(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index b12499c9..a5a475e0 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -13,7 +13,7 @@ def cli() -> click.Group: """ -if __name__ == '__main__': +if __name__ == "__main__": cli.add_command(commands.setup) cli.add_command(commands.config) cli.add_command(commands.submit) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index 2b5dbc5c..4248c049 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -24,7 +24,9 @@ def get_setting(key: str) -> Any: try: configuration = yaml.safe_load(configuration_file.read_text()) except FileNotFoundError: - click.secho("configuration file doesn't exist. Run config command first", fg='red') + click.secho( + "configuration file doesn't exist. Run config command first", fg="red" + ) raise return configuration[key] @@ -40,13 +42,15 @@ def check_path_exists(path: Path, command: click.Command, path_type: str = None) match path_type: case None: if not path.exists(): - click.secho(f"{path} doesn't exist", fg='red') + click.secho(f"{path} doesn't exist", fg="red") case "dir": if not path.is_dir(): - click.secho(f"Directory at {path} doesn't exist or isn't a directory", fg='red') + click.secho( + f"Directory at {path} doesn't exist or isn't a directory", fg="red" + ) case "file": if not path.is_file(): - click.secho(f"File at {path} doesn't exist or isn't a file", fg='red') + click.secho(f"File at {path} doesn't exist or isn't a file", fg="red") case _: return # If everything is OK, exit the function before aborting. click.Context(command).abort() @@ -66,6 +70,8 @@ def send_aoc_request(method, endpoint: str, payload=None) -> str: cookies = {consts.SESSION: session_id} url = urllib.parse.urljoin(consts.BASE_URL, endpoint) - request = requests.request(method, url, headers=consts.USER_AGENT_HEADER, cookies=cookies, data=payload) + request = requests.request( + method, url, headers=consts.USER_AGENT_HEADER, cookies=cookies, data=payload + ) request.raise_for_status() return request.text diff --git a/solution_runner/commands/config_command.py b/solution_runner/commands/config_command.py index f2d05704..0ae2d278 100644 --- a/solution_runner/commands/config_command.py +++ b/solution_runner/commands/config_command.py @@ -7,11 +7,22 @@ from . import consts -@click.command(name='config') -@click.option('--root', 'root_directory', type=consts.ROOT_DIRECTORY_TYPE, prompt=True, prompt_required=False, - help='root directory of Advent of Code puzzles project') -@click.option('--session-id', prompt=True, prompt_required=False, hide_input=True, - help='session ID to access puzzles input') +@click.command(name="config") +@click.option( + "--root", + "root_directory", + type=consts.ROOT_DIRECTORY_TYPE, + prompt=True, + prompt_required=False, + help="root directory of Advent of Code puzzles project", +) +@click.option( + "--session-id", + prompt=True, + prompt_required=False, + hide_input=True, + help="session ID to access puzzles input", +) def command(root_directory: str, session_id: str): """Set options.""" app_data_directory = click.get_app_dir(consts.APP_DATA_DIRECTORY) @@ -35,7 +46,9 @@ def command(root_directory: str, session_id: str): configuration_file.write_text(yaml.dump(configuration)) -def _configure_root_directory(configuration: dict[str, Any], root_directory: str | None) -> str: +def _configure_root_directory( + configuration: dict[str, Any], root_directory: str | None +) -> str: """ Edit the root directory configuration if needed. :param configuration: current configuration @@ -45,8 +58,10 @@ def _configure_root_directory(configuration: dict[str, Any], root_directory: str if root_directory is not None: configuration[consts.ROOT_DIRECTORY] = root_directory elif consts.ROOT_DIRECTORY not in configuration: - configuration[consts.ROOT_DIRECTORY] = click.prompt('Enter path for Advent of Code project root directory', - type=consts.ROOT_DIRECTORY_TYPE) + configuration[consts.ROOT_DIRECTORY] = click.prompt( + "Enter path for Advent of Code project root directory", + type=consts.ROOT_DIRECTORY_TYPE, + ) return configuration[consts.ROOT_DIRECTORY] @@ -60,4 +75,6 @@ def _configure_session_id(configuration: dict[str, Any], session_id: str | None) configuration[consts.SESSION_ID] = session_id elif consts.SESSION_ID not in configuration: configuration[consts.SESSION_ID] = click.prompt( - 'Enter session ID to download input files (available in AoC website cookies)', hide_input=True) + "Enter session ID to download input files (available in AoC website cookies)", + hide_input=True, + ) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 73cda19f..5298cccc 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -9,37 +9,41 @@ FIRST_AOC_YEAR = 2015 DECEMBER = 12 ADVENT_DAYS_RANGE = click.IntRange(1, 25) -ZERO = '0' -BASE_URL = 'https://adventofcode.com/' -INPUT_ENDPOINT_TEMPLATE = string.Template('/$year/day/$day/input') -SUBMIT_ENDPOINT_TEMPLATE = string.Template('/$year/day/$day/answer') -SESSION = 'session' +ZERO = "0" +BASE_URL = "https://adventofcode.com/" +INPUT_ENDPOINT_TEMPLATE = string.Template("/$year/day/$day/input") +SUBMIT_ENDPOINT_TEMPLATE = string.Template("/$year/day/$day/answer") +SESSION = "session" # Requested by Advent of Code owner to help track requests. -USER_AGENT_HEADER = {'User-Agent': 'AoC.CLI.katzuv'} +USER_AGENT_HEADER = {"User-Agent": "AoC.CLI.katzuv"} -APP_DATA_DIRECTORY = click.get_app_dir('Advent of Code') -CONFIGURATION_FILE_NAME = Path('configuration.yaml') -ROOT_DIRECTORY_TYPE = click.Path(file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True) -ROOT_DIRECTORY = 'root directory' -SOLUTION_PARTS = ('p1', 'p2') -SESSION_ID = 'session ID' -SOLUTION_FILE_CONTENT = Path(Path(__file__).parent, 'solution_template.py').read_text() +APP_DATA_DIRECTORY = click.get_app_dir("Advent of Code") +CONFIGURATION_FILE_NAME = Path("configuration.yaml") +ROOT_DIRECTORY_TYPE = click.Path( + file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True +) +ROOT_DIRECTORY = "root directory" +SOLUTION_PARTS = ("p1", "p2") +SESSION_ID = "session ID" +SOLUTION_FILE_CONTENT = Path(Path(__file__).parent, "solution_template.py").read_text() class Directories: - SOLUTIONS = Path('solutions') - INPUTS = Path('inputs') + SOLUTIONS = Path("solutions") + INPUTS = Path("inputs") class FileExtensions: - TEXT = '.txt' - PYTHON = '.py' + TEXT = ".txt" + PYTHON = ".py" class HttpMethods: - GET = 'GET' - POST = 'POST' + GET = "GET" + POST = "POST" -US_EASTERN_TIMEZONE = ZoneInfo('US/Eastern') -AOC_UNLOCK_TIME_TEMPLATE = datetime(year=1, month=12, day=1, hour=0, tzinfo=US_EASTERN_TIMEZONE) +US_EASTERN_TIMEZONE = ZoneInfo("US/Eastern") +AOC_UNLOCK_TIME_TEMPLATE = datetime( + year=1, month=12, day=1, hour=0, tzinfo=US_EASTERN_TIMEZONE +) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index c98da465..18cac2c1 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -11,12 +11,29 @@ _default_year = get_default_year() -@click.command(name='setup') -@click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), default=_default_year, - show_default=f'last year: {_default_year}', help='year of puzzle setting up solution for') -@click.option('-d', '--day', type=consts.ADVENT_DAYS_RANGE, required=True, help='day of puzzle setting up solution for') -@click.option('--use-cache/--ignore-cache', 'should_use_cache', default=True, show_default='true', - help='whether to use cached input file') +@click.command(name="setup") +@click.option( + "-y", + "--year", + type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), + default=_default_year, + show_default=f"last year: {_default_year}", + help="year of puzzle setting up solution for", +) +@click.option( + "-d", + "--day", + type=consts.ADVENT_DAYS_RANGE, + required=True, + help="day of puzzle setting up solution for", +) +@click.option( + "--use-cache/--ignore-cache", + "should_use_cache", + default=True, + show_default="true", + help="whether to use cached input file", +) def command(year: int, day: int, should_use_cache: bool): """Set up a solution: fetch input and create solution files.""" _abort_if_puzzle_locked(year, day) @@ -29,13 +46,15 @@ def command(year: int, day: int, should_use_cache: bool): except FileNotFoundError: click.Context(command).abort() inputs_directory = root_directory / Directories.INPUTS - _ask_user_to_mkdir(inputs_directory, 'input files') + _ask_user_to_mkdir(inputs_directory, "input files") year_inputs_directory = inputs_directory / year - _ask_user_to_mkdir(year_inputs_directory, f'{year} input files') + _ask_user_to_mkdir(year_inputs_directory, f"{year} input files") input_file = (year_inputs_directory / day).with_suffix(FileExtensions.TEXT) - if should_use_cache and input_file.exists() and input_file.read_text(): # Abort if the file exists but it's empty. + if ( + should_use_cache and input_file.exists() and input_file.read_text() + ): # Abort if the file exists but it's empty. _abort_input_file_already_exists(year, day) input_file.touch() @@ -47,10 +66,10 @@ def command(year: int, day: int, should_use_cache: bool): _download_input(year, day, input_file, session_id) solutions_directory = root_directory / Directories.SOLUTIONS - _ask_user_to_mkdir(solutions_directory, 'solution files') + _ask_user_to_mkdir(solutions_directory, "solution files") year_solutions_directory = solutions_directory / year - _ask_user_to_mkdir(year_solutions_directory, f'{year} solution files') + _ask_user_to_mkdir(year_solutions_directory, f"{year} solution files") _create_files(year_solutions_directory, day) @@ -63,7 +82,7 @@ def _abort_if_puzzle_locked(year: int, day: int): puzzle_unlock_time = consts.AOC_UNLOCK_TIME_TEMPLATE.replace(year=year, day=day) now = datetime.now(tz=consts.US_EASTERN_TIMEZONE) if puzzle_unlock_time > now: - click.secho(f"{year}'s day {day} puzzle wasn't unlocked yet.", fg='red') + click.secho(f"{year}'s day {day} puzzle wasn't unlocked yet.", fg="red") click.Context(command).abort() @@ -80,8 +99,13 @@ def _ask_user_to_mkdir(directory: Path, name: str = None): if name is None: name = directory.name - if click.confirm(f"{name} directory doesn't exist, do you want to create it?", prompt_suffix='', default=True, - show_default=True, abort=True): + if click.confirm( + f"{name} directory doesn't exist, do you want to create it?", + prompt_suffix="", + default=True, + show_default=True, + abort=True, + ): directory.mkdir() @@ -92,7 +116,7 @@ def _abort_input_file_already_exists(year: str, day: str): :param day: day of the puzzle """ day = day.lstrip(consts.ZERO) # Remove leading zeros for prettier printing. - click.secho(f"Input file for {year}'s day {day} puzzle already exists.", fg='red') + click.secho(f"Input file for {year}'s day {day} puzzle already exists.", fg="red") click.Context(command).abort() diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index c877e3eb..9406ae23 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -17,19 +17,37 @@ def _get_result(result: str) -> tuple[str, bool]: :param result: result page after submitting an answer :return: tuple of (result description, whether the solution was correct or not) """ - parsed_result = bs4.BeautifulSoup(result, 'html.parser') - result_paragraph = parsed_result.find('p').text - first_sentence = result_paragraph.split('.', maxsplit=1)[0] + '.' + parsed_result = bs4.BeautifulSoup(result, "html.parser") + result_paragraph = parsed_result.find("p").text + first_sentence = result_paragraph.split(".", maxsplit=1)[0] + "." right_answer = "That's the right answer!" in first_sentence return first_sentence, right_answer -@click.command(name='submit') -@click.option('-y', '--year', type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), default=_default_year, - show_default=f'last year: {_default_year}', help='year of puzzle setting up solution for') -@click.option('-d', '--day', type=consts.ADVENT_DAYS_RANGE, required=True, help='day of puzzle to submit') -@click.option('-p', '--part', type=click.Choice(('1', '2')), required=True, help='puzzle part to submit') +@click.command(name="submit") +@click.option( + "-y", + "--year", + type=click.IntRange(consts.FIRST_AOC_YEAR, _default_year), + default=_default_year, + show_default=f"last year: {_default_year}", + help="year of puzzle setting up solution for", +) +@click.option( + "-d", + "--day", + type=consts.ADVENT_DAYS_RANGE, + required=True, + help="day of puzzle to submit", +) +@click.option( + "-p", + "--part", + type=click.Choice(("1", "2")), + required=True, + help="puzzle part to submit", +) def command(year: int, day: int, part: int): """Submit solution.""" year = str(year) @@ -38,16 +56,24 @@ def command(year: int, day: int, part: int): root_directory = get_setting(consts.ROOT_DIRECTORY) solutions_directory = root_directory / Directories.SOLUTIONS / year / f"d{day}" if not solutions_directory.is_dir(): - click.secho("Solution directory does not exist. Run setup command first", fg='red') + click.secho( + "Solution directory does not exist. Run setup command first", fg="red" + ) click.Context(command).abort() - input_path = (root_directory / Directories.INPUTS / year / day).with_suffix(FileExtensions.TEXT) + input_path = (root_directory / Directories.INPUTS / year / day).with_suffix( + FileExtensions.TEXT + ) input_text = input_path.read_text() - part_solution_path = (solutions_directory / f"p{part}").with_suffix(FileExtensions.PYTHON) + part_solution_path = (solutions_directory / f"p{part}").with_suffix( + FileExtensions.PYTHON + ) command_arguments = ("python", part_solution_path, input_text) try: - result = subprocess.run(command_arguments, capture_output=True, check=True, text=True) + result = subprocess.run( + command_arguments, capture_output=True, check=True, text=True + ) # If an error occurred in the called solution file, print the exception and abort. except subprocess.CalledProcessError as error: print(error.stderr) @@ -59,5 +85,5 @@ def command(year: int, day: int, part: int): result = commands_utils.send_aoc_request(HttpMethods.POST, submit_endpoint, body) sentence, is_answer_right = _get_result(result) - color = 'green' if is_answer_right else 'yellow' + color = "green" if is_answer_right else "yellow" click.secho(sentence, fg=color) diff --git a/solution_runner/consts.py b/solution_runner/consts.py index 11a7fb31..6dd8aca8 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,2 +1,2 @@ class CliConstants: - CONTEXT = {'help_option_names': ['-h', '--help']} + CONTEXT = {"help_option_names": ["-h", "--help"]} From c5da269251efc1150653c29c9fbea8143c719048 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 20:32:56 +0200 Subject: [PATCH 155/176] Move `get_default_year` to utils module --- solution_runner/commands/commands_utils.py | 12 ++++++++++++ solution_runner/commands/defaults_and_choices.py | 14 -------------- solution_runner/commands/setup_command.py | 3 +-- solution_runner/commands/submit_command.py | 3 +-- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index 4248c049..ef121aee 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -1,4 +1,5 @@ import urllib.parse +from datetime import datetime from enum import Enum from pathlib import Path from typing import Any @@ -75,3 +76,14 @@ def send_aoc_request(method, endpoint: str, payload=None) -> str: ) request.raise_for_status() return request.text + + +def get_default_year() -> int: + """ + :return: default year which is the current year if it's December, last year otherwise + """ + today = datetime.today() + current_year = today.year + if today.month == consts.DECEMBER: + return current_year + return current_year - 1 diff --git a/solution_runner/commands/defaults_and_choices.py b/solution_runner/commands/defaults_and_choices.py index ece55fa3..e69de29b 100644 --- a/solution_runner/commands/defaults_and_choices.py +++ b/solution_runner/commands/defaults_and_choices.py @@ -1,14 +0,0 @@ -from datetime import datetime - -from . import consts - - -def get_default_year() -> int: - """ - :return: default year which is the current year if it's December, last year otherwise - """ - today = datetime.today() - current_year = today.year - if today.month == consts.DECEMBER: - return current_year - return current_year - 1 diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 18cac2c1..94db9d8c 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -5,10 +5,9 @@ from . import consts, commands_utils from .consts import Directories, FileExtensions, HttpMethods -from .defaults_and_choices import get_default_year -_default_year = get_default_year() +_default_year = commands_utils.get_default_year() @click.command(name="setup") diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 9406ae23..8afa1127 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -6,10 +6,9 @@ from . import consts, commands_utils from .commands_utils import get_setting from .consts import Directories, FileExtensions, HttpMethods -from .defaults_and_choices import get_default_year -_default_year = get_default_year() +_default_year = commands_utils.get_default_year() def _get_result(result: str) -> tuple[str, bool]: From ba7a9474442e39bfd1cd9ca2aadecfa5b77f57a7 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 20:34:01 +0200 Subject: [PATCH 156/176] Remove now empty `defaults_and_choices` module --- solution_runner/commands/defaults_and_choices.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 solution_runner/commands/defaults_and_choices.py diff --git a/solution_runner/commands/defaults_and_choices.py b/solution_runner/commands/defaults_and_choices.py deleted file mode 100644 index e69de29b..00000000 From e22492b366de8957e2e014696e1df781f308fa2b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 20:35:15 +0200 Subject: [PATCH 157/176] Add command-line argument input to solution template --- solution_runner/commands/solution_template.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/solution_runner/commands/solution_template.py b/solution_runner/commands/solution_template.py index 3479ef77..b8027416 100644 --- a/solution_runner/commands/solution_template.py +++ b/solution_runner/commands/solution_template.py @@ -1,2 +1,9 @@ +import sys + + def get_answer(input_text: str): raise NotImplementedError + + +if __name__ == '__main__': + print(get_answer(sys.argv[1])) From 58c140cafed1683f0211b1c8a56d58a2ecb3e1e1 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 20:47:51 +0200 Subject: [PATCH 158/176] Remove unused `session_id` parameter --- solution_runner/commands/setup_command.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 94db9d8c..d45f84cd 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -62,7 +62,7 @@ def command(year: int, day: int, should_use_cache: bool): except FileNotFoundError: click.Context(command).abort() - _download_input(year, day, input_file, session_id) + _download_input(year, day, input_file) solutions_directory = root_directory / Directories.SOLUTIONS _ask_user_to_mkdir(solutions_directory, "solution files") @@ -132,13 +132,12 @@ def _create_files(year_solutions_directory: Path, day: str): filepath.write_text(consts.SOLUTION_FILE_CONTENT) -def _download_input(year: str, day: str, input_file: Path, session_id: str): +def _download_input(year: str, day: str, input_file: Path): """ Download the puzzle's input and write it to a file. :param year: year of the puzzle :param day: day of the puzzle :param input_file: input file path - :param session_id: session ID to download input from website """ day = day.lstrip(consts.ZERO) endpoint = consts.INPUT_ENDPOINT_TEMPLATE.substitute(year=year, day=day) From 9e8a6585182065d6b12c5b2da9410ed6f1dd230b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 20:49:07 +0200 Subject: [PATCH 159/176] Remove unused `session_id` variable --- solution_runner/commands/setup_command.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index d45f84cd..75c902cc 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -57,11 +57,6 @@ def command(year: int, day: int, should_use_cache: bool): _abort_input_file_already_exists(year, day) input_file.touch() - try: - session_id = commands_utils.get_setting(consts.SESSION_ID) - except FileNotFoundError: - click.Context(command).abort() - _download_input(year, day, input_file) solutions_directory = root_directory / Directories.SOLUTIONS From 5cdc01a652d13a0623379b2145c563f29dfe8b73 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 20:50:41 +0200 Subject: [PATCH 160/176] Move CLI context constant to `cli` module --- solution_runner/cli.py | 6 ++++-- solution_runner/consts.py | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/solution_runner/cli.py b/solution_runner/cli.py index a5a475e0..15a96295 100644 --- a/solution_runner/cli.py +++ b/solution_runner/cli.py @@ -1,10 +1,12 @@ import click import commands -from consts import CliConstants -@click.group(context_settings=CliConstants.CONTEXT) +CONTEXT = {"help_option_names": ["-h", "--help"]} + + +@click.group(context_settings=CONTEXT) def cli() -> click.Group: """ Main CLI holding all Advent of Code related commands. diff --git a/solution_runner/consts.py b/solution_runner/consts.py index 6dd8aca8..8b137891 100644 --- a/solution_runner/consts.py +++ b/solution_runner/consts.py @@ -1,2 +1 @@ -class CliConstants: - CONTEXT = {"help_option_names": ["-h", "--help"]} + From af8d5ca7f33125d40d8dd3981142e77dc893e3f6 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 20:51:52 +0200 Subject: [PATCH 161/176] Remove now empty `cli`\`consts` module --- solution_runner/consts.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 solution_runner/consts.py diff --git a/solution_runner/consts.py b/solution_runner/consts.py deleted file mode 100644 index 8b137891..00000000 --- a/solution_runner/consts.py +++ /dev/null @@ -1 +0,0 @@ - From d87a8e8e697c7b6a97b1e4f6018e89709f2563ff Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 20:54:19 +0200 Subject: [PATCH 162/176] Replace `click.Context(command).abort()` with `raise click.Abort()` --- solution_runner/commands/commands_utils.py | 2 +- solution_runner/commands/setup_command.py | 6 +++--- solution_runner/commands/submit_command.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index ef121aee..770f3c3a 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -54,7 +54,7 @@ def check_path_exists(path: Path, command: click.Command, path_type: str = None) click.secho(f"File at {path} doesn't exist or isn't a file", fg="red") case _: return # If everything is OK, exit the function before aborting. - click.Context(command).abort() + raise click.Abort() def send_aoc_request(method, endpoint: str, payload=None) -> str: diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 75c902cc..6b6c35e0 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -43,7 +43,7 @@ def command(year: int, day: int, should_use_cache: bool): try: root_directory = commands_utils.get_setting(consts.ROOT_DIRECTORY) except FileNotFoundError: - click.Context(command).abort() + raise click.Abort() inputs_directory = root_directory / Directories.INPUTS _ask_user_to_mkdir(inputs_directory, "input files") @@ -77,7 +77,7 @@ def _abort_if_puzzle_locked(year: int, day: int): now = datetime.now(tz=consts.US_EASTERN_TIMEZONE) if puzzle_unlock_time > now: click.secho(f"{year}'s day {day} puzzle wasn't unlocked yet.", fg="red") - click.Context(command).abort() + raise click.Abort() def _ask_user_to_mkdir(directory: Path, name: str = None): @@ -111,7 +111,7 @@ def _abort_input_file_already_exists(year: str, day: str): """ day = day.lstrip(consts.ZERO) # Remove leading zeros for prettier printing. click.secho(f"Input file for {year}'s day {day} puzzle already exists.", fg="red") - click.Context(command).abort() + raise click.Abort() def _create_files(year_solutions_directory: Path, day: str): diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 8afa1127..49e2abca 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -58,7 +58,7 @@ def command(year: int, day: int, part: int): click.secho( "Solution directory does not exist. Run setup command first", fg="red" ) - click.Context(command).abort() + raise click.Abort() input_path = (root_directory / Directories.INPUTS / year / day).with_suffix( FileExtensions.TEXT From da6b4a2da82b182a9704e387781ac2247b101c2b Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 21:07:19 +0200 Subject: [PATCH 163/176] Add function that runs the solution module and returns the answer --- solution_runner/commands/submit_command.py | 33 +++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 49e2abca..2655cab6 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -1,4 +1,5 @@ import subprocess +from pathlib import Path import bs4 import click @@ -64,11 +65,29 @@ def command(year: int, day: int, part: int): FileExtensions.TEXT ) input_text = input_path.read_text() - - part_solution_path = (solutions_directory / f"p{part}").with_suffix( + solution_path = (root_directory / Directories.SOLUTIONS / year / day).with_suffix( FileExtensions.PYTHON ) - command_arguments = ("python", part_solution_path, input_text) + answer = _get_answer(input_text, solution_path) + + body = {"level": str(part), "answer": answer} + submit_endpoint = consts.SUBMIT_ENDPOINT_TEMPLATE.substitute(year=year, day=day) + result = commands_utils.send_aoc_request(HttpMethods.POST, submit_endpoint, body) + + sentence, is_answer_right = _get_result(result) + color = "green" if is_answer_right else "yellow" + click.secho(sentence, fg=color) + + +def _get_answer(input_text: str, solution_path: Path) -> str: + """ + Run the solution module and return the answer. + :param input_text: input to pass to the solution module + :param solution_path: path to the Python solution module + :return: puzzle answer + :raise: `click.Abort` if an error occurred while running the solution module + """ + command_arguments = ("python", solution_path, input_text) try: result = subprocess.run( command_arguments, capture_output=True, check=True, text=True @@ -79,10 +98,4 @@ def command(year: int, day: int, part: int): raise click.Abort() solution = result.stdout.strip() - body = {"level": str(part), "answer": solution} - submit_endpoint = consts.SUBMIT_ENDPOINT_TEMPLATE.substitute(year=year, day=day) - result = commands_utils.send_aoc_request(HttpMethods.POST, submit_endpoint, body) - - sentence, is_answer_right = _get_result(result) - color = "green" if is_answer_right else "yellow" - click.secho(sentence, fg=color) + return solution From 23bd7dd74dbff39e91495602983f4c7b0e7539ff Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 21:11:46 +0200 Subject: [PATCH 164/176] Add function that submit the answer and returns the result --- solution_runner/commands/submit_command.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 2655cab6..25c15ef8 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -69,16 +69,28 @@ def command(year: int, day: int, part: int): FileExtensions.PYTHON ) answer = _get_answer(input_text, solution_path) - - body = {"level": str(part), "answer": answer} - submit_endpoint = consts.SUBMIT_ENDPOINT_TEMPLATE.substitute(year=year, day=day) - result = commands_utils.send_aoc_request(HttpMethods.POST, submit_endpoint, body) + result = _get_result_from_website(year, day, part, answer) sentence, is_answer_right = _get_result(result) color = "green" if is_answer_right else "yellow" click.secho(sentence, fg=color) +def _get_result_from_website(year: str, day: str, part: int, answer: str) -> str: + """ + Submit the answer to the website and return its response to it. + :param year: year of the puzzle + :param day: day of the puzzle + :param part: part of the day's puzzle + :param answer: puzzle answer to submit + :return: response from the website + """ + body = {"level": str(part), "answer": answer} + submit_endpoint = consts.SUBMIT_ENDPOINT_TEMPLATE.substitute(year=year, day=day) + result = commands_utils.send_aoc_request(HttpMethods.POST, submit_endpoint, body) + return result + + def _get_answer(input_text: str, solution_path: Path) -> str: """ Run the solution module and return the answer. From 7b1f5dbfe585a3fbb7146cbb1f4f5ec5c6cc60b3 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 21:13:10 +0200 Subject: [PATCH 165/176] Rename `_get_result` to `_parse_result` for better clarity --- solution_runner/commands/submit_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 25c15ef8..180a1b02 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -12,7 +12,7 @@ _default_year = commands_utils.get_default_year() -def _get_result(result: str) -> tuple[str, bool]: +def _parse_result(result: str) -> tuple[str, bool]: """ :param result: result page after submitting an answer :return: tuple of (result description, whether the solution was correct or not) From 14d256dd4b45656a39d7f267b4ebea405e6c23b3 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 18 Nov 2022 21:24:34 +0200 Subject: [PATCH 166/176] Add function that styles and prints the solution result from the website --- solution_runner/commands/submit_command.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/submit_command.py b/solution_runner/commands/submit_command.py index 180a1b02..12d2eb50 100644 --- a/solution_runner/commands/submit_command.py +++ b/solution_runner/commands/submit_command.py @@ -70,8 +70,16 @@ def command(year: int, day: int, part: int): ) answer = _get_answer(input_text, solution_path) result = _get_result_from_website(year, day, part, answer) + parsed_result = _parse_result(result) + _print_result(parsed_result) - sentence, is_answer_right = _get_result(result) + +def _print_result(result: tuple[str, bool]) -> None: + """ + Print submit result retrieved from the website. + :param result: result retrieved from the website + """ + sentence, is_answer_right = result color = "green" if is_answer_right else "yellow" click.secho(sentence, fg=color) From 6cad3b774ca5c2a25d291f34f8c5f83854198298 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Mon, 21 Nov 2022 19:31:28 +0200 Subject: [PATCH 167/176] Remove unused parameter --- solution_runner/commands/commands_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index 770f3c3a..b40ea51b 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -33,11 +33,10 @@ def get_setting(key: str) -> Any: return configuration[key] -def check_path_exists(path: Path, command: click.Command, path_type: str = None): +def check_path_exists(path: Path, path_type: str = None): """ Abort if path doesn't exist. :param path: path to check - :param command: command to be passed to`click.Context` if aborting is needed :param path_type: optional "dir" or "file". If not passed, only check for existence. If passed, check for type. """ match path_type: From afdec7d2633674ffbe9134d9aa6e10a946b6f93d Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 9 Dec 2022 17:07:14 +0200 Subject: [PATCH 168/176] Remove unused function --- solution_runner/commands/commands_utils.py | 24 ---------------------- 1 file changed, 24 deletions(-) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index b40ea51b..f8860747 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -1,7 +1,6 @@ import urllib.parse from datetime import datetime from enum import Enum -from pathlib import Path from typing import Any import click @@ -33,29 +32,6 @@ def get_setting(key: str) -> Any: return configuration[key] -def check_path_exists(path: Path, path_type: str = None): - """ - Abort if path doesn't exist. - :param path: path to check - :param path_type: optional "dir" or "file". If not passed, only check for existence. If passed, check for type. - """ - match path_type: - case None: - if not path.exists(): - click.secho(f"{path} doesn't exist", fg="red") - case "dir": - if not path.is_dir(): - click.secho( - f"Directory at {path} doesn't exist or isn't a directory", fg="red" - ) - case "file": - if not path.is_file(): - click.secho(f"File at {path} doesn't exist or isn't a file", fg="red") - case _: - return # If everything is OK, exit the function before aborting. - raise click.Abort() - - def send_aoc_request(method, endpoint: str, payload=None) -> str: """ Send a request to Advent of Code's website and return the textual response. From 8c3375ecd9ce620494533458a1dda26110db6dd5 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 9 Dec 2022 17:28:33 +0200 Subject: [PATCH 169/176] Removed redundant file creation before writing to it --- solution_runner/commands/setup_command.py | 1 - 1 file changed, 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 6b6c35e0..aa7af94f 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -55,7 +55,6 @@ def command(year: int, day: int, should_use_cache: bool): should_use_cache and input_file.exists() and input_file.read_text() ): # Abort if the file exists but it's empty. _abort_input_file_already_exists(year, day) - input_file.touch() _download_input(year, day, input_file) From 39984fc85ac50c131c89d9d0639635627f032767 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 9 Dec 2022 17:32:45 +0200 Subject: [PATCH 170/176] Save solution file template path and not content --- solution_runner/commands/consts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/consts.py b/solution_runner/commands/consts.py index 5298cccc..1c2d6a50 100644 --- a/solution_runner/commands/consts.py +++ b/solution_runner/commands/consts.py @@ -25,7 +25,7 @@ ROOT_DIRECTORY = "root directory" SOLUTION_PARTS = ("p1", "p2") SESSION_ID = "session ID" -SOLUTION_FILE_CONTENT = Path(Path(__file__).parent, "solution_template.py").read_text() +SOLUTION_FILE_TEMPLATE_PATH = Path(Path(__file__).parent, "solution_template.py") class Directories: From 6566a9a29971e16f5a8a57dd518894ab25334c6e Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 9 Dec 2022 17:53:24 +0200 Subject: [PATCH 171/176] Copy file directly instead of reading and writing --- solution_runner/commands/setup_command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index aa7af94f..ca727c7e 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -1,3 +1,4 @@ +import shutil from datetime import datetime from pathlib import Path @@ -123,7 +124,7 @@ def _create_files(year_solutions_directory: Path, day: str): solutions_directory.mkdir() for part in consts.SOLUTION_PARTS: filepath = (solutions_directory / part).with_suffix(FileExtensions.PYTHON) - filepath.write_text(consts.SOLUTION_FILE_CONTENT) + shutil.copy(consts.SOLUTION_FILE_TEMPLATE_PATH, filepath) def _download_input(year: str, day: str, input_file: Path): From 962dc92211ce5d603f3e2c93a880e42f4aa962d1 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 9 Dec 2022 19:26:47 +0200 Subject: [PATCH 172/176] Abort if HTTP request was unsuccessful --- solution_runner/commands/commands_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/commands_utils.py b/solution_runner/commands/commands_utils.py index f8860747..7f7efe95 100644 --- a/solution_runner/commands/commands_utils.py +++ b/solution_runner/commands/commands_utils.py @@ -49,7 +49,9 @@ def send_aoc_request(method, endpoint: str, payload=None) -> str: request = requests.request( method, url, headers=consts.USER_AGENT_HEADER, cookies=cookies, data=payload ) - request.raise_for_status() + if not request.ok: + click.secho(request.text, fg="red") + raise click.Abort() return request.text From e4719a1a1e77d85cb5e53b765273383c8d4e5ff3 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 9 Dec 2022 20:02:20 +0200 Subject: [PATCH 173/176] Refactor checking whether the file is empty --- solution_runner/commands/setup_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index ca727c7e..a96e97ac 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -53,7 +53,7 @@ def command(year: int, day: int, should_use_cache: bool): input_file = (year_inputs_directory / day).with_suffix(FileExtensions.TEXT) if ( - should_use_cache and input_file.exists() and input_file.read_text() + should_use_cache and input_file.exists() and input_file.stat().st_size == 0 ): # Abort if the file exists but it's empty. _abort_input_file_already_exists(year, day) From ee774339ab5b22f0344397f4bcd5371b7c4605b0 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 9 Dec 2022 20:03:34 +0200 Subject: [PATCH 174/176] Fix comment --- solution_runner/commands/setup_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index a96e97ac..c6e58c3b 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -54,7 +54,7 @@ def command(year: int, day: int, should_use_cache: bool): if ( should_use_cache and input_file.exists() and input_file.stat().st_size == 0 - ): # Abort if the file exists but it's empty. + ): # Don't abort if the file exists, but it's empty. _abort_input_file_already_exists(year, day) _download_input(year, day, input_file) From f3e1392c9fbc3e0e9368e6f075355b9441da9165 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 9 Dec 2022 20:07:00 +0200 Subject: [PATCH 175/176] Refactor condition --- solution_runner/commands/setup_command.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index c6e58c3b..495f840d 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -53,8 +53,11 @@ def command(year: int, day: int, should_use_cache: bool): input_file = (year_inputs_directory / day).with_suffix(FileExtensions.TEXT) if ( - should_use_cache and input_file.exists() and input_file.stat().st_size == 0 - ): # Don't abort if the file exists, but it's empty. + should_use_cache + and input_file.exists() + and input_file.stat().st_size + == 0 # Don't abort if the file exists, but it's empty. + ): _abort_input_file_already_exists(year, day) _download_input(year, day, input_file) From d4b951091cdfa26ec2d295f4ea3b139dce89c7e3 Mon Sep 17 00:00:00 2001 From: Dan Katzuv Date: Fri, 9 Dec 2022 20:18:22 +0200 Subject: [PATCH 176/176] Don't create solution files if they already exist --- solution_runner/commands/setup_command.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/solution_runner/commands/setup_command.py b/solution_runner/commands/setup_command.py index 495f840d..681ddd40 100644 --- a/solution_runner/commands/setup_command.py +++ b/solution_runner/commands/setup_command.py @@ -124,7 +124,12 @@ def _create_files(year_solutions_directory: Path, day: str): :param day: day of the challenge """ solutions_directory = year_solutions_directory / f"d{day}" - solutions_directory.mkdir() + try: + solutions_directory.mkdir() + except FileExistsError as error: + if error.errno == errno.EEXIST: + return # If solution directory already exists, don't create new solution files. + raise for part in consts.SOLUTION_PARTS: filepath = (solutions_directory / part).with_suffix(FileExtensions.PYTHON) shutil.copy(consts.SOLUTION_FILE_TEMPLATE_PATH, filepath)