Skip to content

Commit d7ab742

Browse files
authored
Merge pull request #4 from katzuv/feature/add-solution-runner
Create solution runner
2 parents 6e30ed2 + d4b9510 commit d7ab742

File tree

10 files changed

+506
-78
lines changed

10 files changed

+506
-78
lines changed

input_getter.py

Lines changed: 0 additions & 78 deletions
This file was deleted.

requirements.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
requests
2+
click
3+
pyyaml
4+
beautifulsoup4
5+
black

solution_runner/cli.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import click
2+
3+
import commands
4+
5+
6+
CONTEXT = {"help_option_names": ["-h", "--help"]}
7+
8+
9+
@click.group(context_settings=CONTEXT)
10+
def cli() -> click.Group:
11+
"""
12+
Main CLI holding all Advent of Code related commands.
13+
14+
For more information about Advent of Code, see https://adventofcode.com.
15+
"""
16+
17+
18+
if __name__ == "__main__":
19+
cli.add_command(commands.setup)
20+
cli.add_command(commands.config)
21+
cli.add_command(commands.submit)
22+
cli()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .config_command import command as config
2+
from .setup_command import command as setup
3+
from .submit_command import command as submit
4+
5+
6+
__all__ = ["config", "setup", "submit"]
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import urllib.parse
2+
from datetime import datetime
3+
from enum import Enum
4+
from typing import Any
5+
6+
import click
7+
import requests
8+
import yaml
9+
10+
from . import consts
11+
12+
13+
class PathType(Enum):
14+
FILE = 1
15+
DIRECTORY = 2
16+
17+
18+
def get_setting(key: str) -> Any:
19+
"""
20+
:param key: key to retrieve its value
21+
:return: value of `key` which is stored in the configuration file
22+
"""
23+
configuration_file = consts.APP_DATA_DIRECTORY / consts.CONFIGURATION_FILE_NAME
24+
try:
25+
configuration = yaml.safe_load(configuration_file.read_text())
26+
except FileNotFoundError:
27+
click.secho(
28+
"configuration file doesn't exist. Run config command first", fg="red"
29+
)
30+
raise
31+
32+
return configuration[key]
33+
34+
35+
def send_aoc_request(method, endpoint: str, payload=None) -> str:
36+
"""
37+
Send a request to Advent of Code's website and return the textual response.
38+
:param method: method of the request
39+
:param endpoint: endpoint to append to the base URL
40+
:param payload: optional payload to attach to the request
41+
:return: text content of the response
42+
"""
43+
if payload is None:
44+
payload = {}
45+
session_id = get_setting(consts.SESSION_ID)
46+
cookies = {consts.SESSION: session_id}
47+
url = urllib.parse.urljoin(consts.BASE_URL, endpoint)
48+
49+
request = requests.request(
50+
method, url, headers=consts.USER_AGENT_HEADER, cookies=cookies, data=payload
51+
)
52+
if not request.ok:
53+
click.secho(request.text, fg="red")
54+
raise click.Abort()
55+
return request.text
56+
57+
58+
def get_default_year() -> int:
59+
"""
60+
:return: default year which is the current year if it's December, last year otherwise
61+
"""
62+
today = datetime.today()
63+
current_year = today.year
64+
if today.month == consts.DECEMBER:
65+
return current_year
66+
return current_year - 1
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from pathlib import Path
2+
from typing import Any
3+
4+
import click
5+
import yaml
6+
7+
from . import consts
8+
9+
10+
@click.command(name="config")
11+
@click.option(
12+
"--root",
13+
"root_directory",
14+
type=consts.ROOT_DIRECTORY_TYPE,
15+
prompt=True,
16+
prompt_required=False,
17+
help="root directory of Advent of Code puzzles project",
18+
)
19+
@click.option(
20+
"--session-id",
21+
prompt=True,
22+
prompt_required=False,
23+
hide_input=True,
24+
help="session ID to access puzzles input",
25+
)
26+
def command(root_directory: str, session_id: str):
27+
"""Set options."""
28+
app_data_directory = click.get_app_dir(consts.APP_DATA_DIRECTORY)
29+
configuration_file = Path(app_data_directory, consts.CONFIGURATION_FILE_NAME)
30+
try:
31+
# Use an empty dictionary if no actual configuration is in the configuration file.
32+
configuration = yaml.safe_load(configuration_file.read_text()) or {}
33+
except FileNotFoundError:
34+
Path(app_data_directory).mkdir(exist_ok=True)
35+
configuration_file.touch()
36+
configuration = {}
37+
initial_configuration = configuration.copy()
38+
39+
root_directory = _configure_root_directory(configuration, root_directory)
40+
root_directory = Path(root_directory)
41+
root_directory.mkdir(exist_ok=True)
42+
43+
_configure_session_id(configuration, session_id)
44+
45+
if configuration != initial_configuration:
46+
configuration_file.write_text(yaml.dump(configuration))
47+
48+
49+
def _configure_root_directory(
50+
configuration: dict[str, Any], root_directory: str | None
51+
) -> str:
52+
"""
53+
Edit the root directory configuration if needed.
54+
:param configuration: current configuration
55+
:param root_directory: root directory passed by the user, `None` if wasn't passed
56+
:return: root directory after configuration if needed
57+
"""
58+
if root_directory is not None:
59+
configuration[consts.ROOT_DIRECTORY] = root_directory
60+
elif consts.ROOT_DIRECTORY not in configuration:
61+
configuration[consts.ROOT_DIRECTORY] = click.prompt(
62+
"Enter path for Advent of Code project root directory",
63+
type=consts.ROOT_DIRECTORY_TYPE,
64+
)
65+
return configuration[consts.ROOT_DIRECTORY]
66+
67+
68+
def _configure_session_id(configuration: dict[str, Any], session_id: str | None):
69+
"""
70+
Configure the session ID if needed.
71+
:param configuration: current configuration
72+
:param session_id: session ID passed by the user, `None` if wasn't passed
73+
"""
74+
if session_id is not None:
75+
configuration[consts.SESSION_ID] = session_id
76+
elif consts.SESSION_ID not in configuration:
77+
configuration[consts.SESSION_ID] = click.prompt(
78+
"Enter session ID to download input files (available in AoC website cookies)",
79+
hide_input=True,
80+
)

solution_runner/commands/consts.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import string
2+
from datetime import datetime
3+
from pathlib import Path
4+
from zoneinfo import ZoneInfo
5+
6+
import click
7+
8+
9+
FIRST_AOC_YEAR = 2015
10+
DECEMBER = 12
11+
ADVENT_DAYS_RANGE = click.IntRange(1, 25)
12+
ZERO = "0"
13+
BASE_URL = "https://adventofcode.com/"
14+
INPUT_ENDPOINT_TEMPLATE = string.Template("/$year/day/$day/input")
15+
SUBMIT_ENDPOINT_TEMPLATE = string.Template("/$year/day/$day/answer")
16+
SESSION = "session"
17+
# Requested by Advent of Code owner to help track requests.
18+
USER_AGENT_HEADER = {"User-Agent": "AoC.CLI.katzuv"}
19+
20+
APP_DATA_DIRECTORY = click.get_app_dir("Advent of Code")
21+
CONFIGURATION_FILE_NAME = Path("configuration.yaml")
22+
ROOT_DIRECTORY_TYPE = click.Path(
23+
file_okay=False, dir_okay=True, writable=True, readable=True, resolve_path=True
24+
)
25+
ROOT_DIRECTORY = "root directory"
26+
SOLUTION_PARTS = ("p1", "p2")
27+
SESSION_ID = "session ID"
28+
SOLUTION_FILE_TEMPLATE_PATH = Path(Path(__file__).parent, "solution_template.py")
29+
30+
31+
class Directories:
32+
SOLUTIONS = Path("solutions")
33+
INPUTS = Path("inputs")
34+
35+
36+
class FileExtensions:
37+
TEXT = ".txt"
38+
PYTHON = ".py"
39+
40+
41+
class HttpMethods:
42+
GET = "GET"
43+
POST = "POST"
44+
45+
46+
US_EASTERN_TIMEZONE = ZoneInfo("US/Eastern")
47+
AOC_UNLOCK_TIME_TEMPLATE = datetime(
48+
year=1, month=12, day=1, hour=0, tzinfo=US_EASTERN_TIMEZONE
49+
)

0 commit comments

Comments
 (0)