-
Notifications
You must be signed in to change notification settings - Fork 0
Create solution runner #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1563d01
51ccb61
3484058
b892fb7
a60498d
1d62c60
f3101e4
fcce4ed
d82c13e
206d74c
68280f0
dc56b01
b43f2b4
80c1cb1
3a21fa7
ca658e0
48bdddb
9ae190e
024436e
14e83ae
b7d5a2e
06572e7
d07aaad
84e8953
26e2ea7
3e0dae0
45802a6
f41bb5e
2e11b7f
bba1781
f55de9d
a7af43e
c7e1f40
5370b5c
12c4e2c
afe0268
6571b77
55a4beb
3fda426
11d6e9a
6cb3e7a
c703c29
a776f90
4b72816
2f493da
e3d752a
fc71689
b063f7b
91852d9
97a4aa8
4f40076
e2f5b53
38857bf
936a9e0
31e3e49
623a455
eb71e53
a852789
011e595
afa4c52
61e974a
3848c53
d9c68dd
78789ea
fd69ac0
20864ed
44a729a
eecd20c
c422fda
db9e7bd
52ca64f
da87fac
72239d4
eac5540
9972433
6c1ecd3
997c92b
0b8af2e
f7def1e
2739e3a
cf536d2
a87c6bb
57e8d18
382605e
71c62c7
226fc7c
3f1344a
24e98bb
64e876b
715254c
412a5dc
999370f
5b16553
9ede193
50a719b
05594f2
227459d
bf976c1
0e618b8
edce6fc
9e6fa67
b8309e3
6cbc754
0c1770f
0febc18
553cd4f
d6910b6
92a3b40
d6fb354
8a15463
078e449
3bd174d
a141d06
7b1aba3
0ac571e
aba9582
8594d00
f09a1b3
b648ca1
167a922
f2769ac
845ffec
14a571a
0845ed5
1848beb
5d902dd
06f0107
fb39ea3
996e43b
e6e70f6
fec4225
1865258
ced68d4
22562d6
2cecc69
3ab890d
36046b1
01eb7e6
426ca77
308d1fd
0cda252
040de2f
4828353
e915df2
530168a
1774351
6ea814b
b3bc555
5f27cf1
6bdb3e4
45cb476
8207d97
4f85447
d0ee8ac
c5da269
ba7a947
e22492b
58c140c
9e8a658
5cdc01a
af8d5ca
d87a8e8
da6b4a2
23bd7dd
7b1f5db
14d256d
6cad3b7
afdec7d
8c3375e
39984fc
6566a9a
962dc92
e4719a1
ee77433
f3e1392
d4b9510
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| requests | ||
| click | ||
| pyyaml | ||
| beautifulsoup4 | ||
| black |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import click | ||
|
|
||
| import commands | ||
|
|
||
|
|
||
| CONTEXT = {"help_option_names": ["-h", "--help"]} | ||
|
|
||
|
|
||
| @click.group(context_settings=CONTEXT) | ||
| def cli() -> click.Group: | ||
| """ | ||
| Main CLI holding all Advent of Code related commands. | ||
|
|
||
| For more information about Advent of Code, see https://adventofcode.com. | ||
| """ | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| cli.add_command(commands.setup) | ||
| cli.add_command(commands.config) | ||
| cli.add_command(commands.submit) | ||
| cli() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import urllib.parse | ||
| from datetime import datetime | ||
| from enum import Enum | ||
| from typing import Any | ||
|
|
||
| import click | ||
| import requests | ||
| import yaml | ||
|
|
||
| from . import consts | ||
|
|
||
|
|
||
| class PathType(Enum): | ||
| FILE = 1 | ||
| DIRECTORY = 2 | ||
|
|
||
|
|
||
| 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] | ||
|
|
||
|
|
||
| def send_aoc_request(method, endpoint: str, payload=None) -> str: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace with a function per HTTP method. This way the GET function does not need to take an optional payload parameter.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? This is a utility function,
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| """ | ||
| 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, headers=consts.USER_AGENT_HEADER, cookies=cookies, data=payload | ||
| ) | ||
| if not request.ok: | ||
| click.secho(request.text, fg="red") | ||
| raise click.Abort() | ||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| import click | ||
| import yaml | ||
|
|
||
| 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", | ||
| ) | ||
| 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 = {} | ||
| initial_configuration = configuration.copy() | ||
|
|
||
| root_directory = _configure_root_directory(configuration, root_directory) | ||
| root_directory = Path(root_directory) | ||
| root_directory.mkdir(exist_ok=True) | ||
|
|
||
| _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: | ||
| """ | ||
| 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] | ||
|
|
||
|
|
||
| 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, | ||
YoniKF marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import string | ||
| from datetime import datetime | ||
| from pathlib import Path | ||
| from zoneinfo import ZoneInfo | ||
|
|
||
| import click | ||
|
|
||
|
|
||
| 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" | ||
| # 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") | ||
| 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_TEMPLATE_PATH = Path(Path(__file__).parent, "solution_template.py") | ||
|
|
||
|
|
||
| class Directories: | ||
| SOLUTIONS = Path("solutions") | ||
| INPUTS = Path("inputs") | ||
|
|
||
|
|
||
| class FileExtensions: | ||
| TEXT = ".txt" | ||
| 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 | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you are getting more than one settings per program execution, it might be better to load the configuration only once and cache the dict in memory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Global variable, or defining a function performing
yaml.safe_load(configuration_file.read_text())and decorating it with@functools.cache.