Skip to content

Commit 531fd60

Browse files
committed
first version
0 parents  commit 531fd60

File tree

8 files changed

+996
-0
lines changed

8 files changed

+996
-0
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Remove Python compiled files
2+
*/__pycache__
3+
4+
# IDE config files
5+
.idea/
6+
7+
# Package build folders
8+
adventofcode_initializer.egg-info/
9+
build/
10+
dist/

COPYING

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include COPYING

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Advent of Code Initializer
2+
3+
This utility allows downloading [Advent of Code](https://adventofcode.com) problems as markdown files. It also
4+
downloads the problems' inputs. This tool creates a folder for the required problem and stores the markdown
5+
and the input files.
6+
7+
## Usage
8+
9+
```
10+
usage: adventofcode_initializer.py [-h] {download,set-session-cookie} ...
11+
12+
Download Advent of Code problems as markdown files and also its inputs
13+
14+
positional arguments:
15+
{download,set-session-cookie}
16+
download Download files
17+
set-session-cookie Set the necessary cookie to download personal inputs
18+
or ploblems' part 2
19+
20+
options:
21+
-h, --help show this help message and exit
22+
23+
In order to download inputs or part 2, you have to set the 'session' cookie.
24+
```
25+
26+
```
27+
usage: adventofcode_initializer.py set-session-cookie [-h] session-cookie
28+
29+
positional arguments:
30+
session-cookie Cookie required to download inputs or problems' part 2
31+
32+
options:
33+
-h, --help show this help message and exit
34+
35+
You only have to do save it once
36+
```
37+
38+
```
39+
usage: adventofcode_initializer.py download [-h] [-a] [-d [1-25]] [-y YEAR]
40+
[--both-parts] [--part-2]
41+
42+
options:
43+
-h, --help show this help message and exit
44+
-a, --all-days Download all problems from a given year
45+
-d [1-25], --day [1-25]
46+
The problem that is going to be downloaded
47+
-y YEAR, --year YEAR Advent of Code edition
48+
--both-parts Download both parts of the problem and its input (if
49+
it is possible)
50+
--part-2 Download part two for the given problem and its input
51+
(if it is possible). It appends to part one's README
52+
if it exists
53+
```
54+
55+
## Installation

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[build-system]
2+
requires = [
3+
'setuptools>=54',
4+
'wheel'
5+
]
6+
build-backend = 'setuptools.build_meta'

setup.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from setuptools import setup, find_packages
2+
3+
with open('README.md', mode='r') as file:
4+
long_description = file.read()
5+
6+
with open('COPYING', mode='r') as file:
7+
license = file.read()
8+
9+
setup(
10+
name='adventofcode-initializer',
11+
version='1.0.0',
12+
license=license,
13+
author='Sergio Marín Sánchez',
14+
author_email='[email protected]',
15+
description='Download Advent of Code problems as markdown files and also its inputs',
16+
long_description=long_description,
17+
url='https://github.com/Serms1999/advent-initializer',
18+
packages=find_packages(),
19+
python_requires='>=3.6',
20+
classifiers=[
21+
'Programming Language :: Python :: 3',
22+
'License :: OSI Approved :: GNU GPLv3',
23+
'Operation System :: OS Independent'
24+
],
25+
entry_points={
26+
'console_scripts': {'adventofcode-initializer = src.__main__:main'}
27+
}
28+
)

src/__init__.py

Whitespace-only changes.

src/__main__.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import argparse
2+
import datetime
3+
import json
4+
import os
5+
import re
6+
import requests
7+
import sys
8+
9+
from bs4 import BeautifulSoup
10+
from markdownify import markdownify as md
11+
12+
13+
CONFIG_FILE_PATH = os.path.join(os.path.expanduser('~'), '.config', 'advent-initializer')
14+
CONFIG_FILENAME = 'session.cfg'
15+
16+
17+
def parse_args(current_year: int) -> (argparse.ArgumentParser, argparse.ArgumentParser):
18+
parser = argparse.ArgumentParser(
19+
description='Download Advent of Code problems as markdown files and also its inputs',
20+
add_help=True,
21+
epilog='In order to download inputs or part 2, you have to set the \'session\' cookie.'
22+
)
23+
24+
subparsers = parser.add_subparsers()
25+
26+
download = subparsers.add_parser(
27+
'download',
28+
help='Download files',
29+
add_help=True
30+
)
31+
32+
download.add_argument(
33+
'-a', '--all-days',
34+
help='Download all problems from a given year',
35+
action='store_true', required=False
36+
)
37+
38+
download.add_argument(
39+
'-d', '--day',
40+
help='The problem that is going to be downloaded',
41+
action='store', type=int, choices=range(1, 26), metavar='[1-25]', required=False
42+
)
43+
44+
download.add_argument(
45+
'-y', '--year',
46+
help='Advent of Code edition',
47+
action='store', type=int, default=current_year
48+
)
49+
50+
download.add_argument(
51+
'--both-parts',
52+
help='Download both parts of the problem and its input (if it is possible)',
53+
action='store_true', required=False
54+
)
55+
56+
download.add_argument(
57+
'--part-2',
58+
help='Download part two for the given problem and its input (if it is possible). It appends to part one\'s '
59+
'README if it exists',
60+
action='store_true', required=False
61+
)
62+
63+
set_cookie = subparsers.add_parser(
64+
'set-session-cookie',
65+
help='Set the necessary cookie to download personal inputs or ploblems\' part 2',
66+
add_help=True,
67+
epilog='You only have to do save it once'
68+
)
69+
70+
set_cookie.add_argument(
71+
'session_cookie',
72+
help='Cookie required to download inputs or problems\' part 2',
73+
action='store', type=str, metavar='session-cookie'
74+
)
75+
76+
return parser.parse_args(), parser
77+
78+
79+
def error_args(message: str) -> None:
80+
print(message, file=sys.stderr)
81+
sys.exit(2)
82+
83+
84+
def get_dates(args: argparse.Namespace, current_year: int) -> tuple:
85+
if args.day is None and not args.all_days:
86+
error_args('\'--day\' and \'--all-days\' can not be simultaneously ignored')
87+
elif args.all_days:
88+
if args.day is not None:
89+
error_args('\'--day\' and \'--all-days\' can not be used simultaneously')
90+
days = list(range(1, 26))
91+
else:
92+
days = [args.day]
93+
94+
year = args.year
95+
96+
if year > current_year:
97+
error_args('\'year\' can not be posterior to the current year')
98+
99+
return days, year
100+
101+
102+
def get_session_cookie() -> dict:
103+
try:
104+
with open(os.path.join(CONFIG_FILE_PATH, CONFIG_FILENAME), mode='r') as config_file:
105+
cookies = json.load(config_file)
106+
except FileNotFoundError:
107+
print('Warning: \'session\' cookie not set', file=sys.stderr)
108+
cookies = {}
109+
except json.decoder.JSONDecodeError:
110+
print('Error: config file bad formatted', file=sys.stderr)
111+
sys.exit(2)
112+
113+
return cookies
114+
115+
116+
def set_session_option(cookie: str) -> None:
117+
cookies = {'session': cookie}
118+
os.makedirs(CONFIG_FILE_PATH, exist_ok=True)
119+
filepath = os.path.join(CONFIG_FILE_PATH, CONFIG_FILENAME)
120+
with open(filepath, mode='wt') as config_file:
121+
json.dump(cookies, config_file, indent=4)
122+
print(f'{filepath} successfully created')
123+
124+
125+
def get_parts_to_download(both: bool, part_2: bool) -> list:
126+
if both:
127+
return [1, 2]
128+
elif part_2:
129+
return [2]
130+
return [1]
131+
132+
133+
def replace_internal_links(markdown_text: str) -> str:
134+
def add_advent_url(match):
135+
return f'<a href=\"https://adventofcode.com{match.group(1)}\"'
136+
137+
return re.sub(pattern=r'<a href=\"(\/.*)\"', repl=add_advent_url, string=markdown_text)
138+
139+
140+
def remove_last_code_line(markdown_text: str) -> str:
141+
return re.sub(pattern=r'\n</code></pre>', repl='</code></pre>', string=markdown_text)
142+
143+
144+
def fix_emphasize_code(markdown_text: str) -> str:
145+
def interchange(match):
146+
return f'<em><code>{match.group(1)}</code></em>'
147+
148+
return re.sub(pattern=r'<code><em>(.*)</em></code>', repl=interchange, string=markdown_text)
149+
150+
151+
def fix_html_format(markdown_text: str) -> str:
152+
fix_links = replace_internal_links(markdown_text)
153+
fix_code_section = remove_last_code_line(fix_links)
154+
return fix_emphasize_code(fix_code_section)
155+
156+
157+
def download_input(day: int, url: str, cookies: dict) -> None:
158+
url_input = url + '/input'
159+
replay = requests.get(url_input, cookies=cookies)
160+
161+
with open(os.path.join(os.getcwd(), f'Day{day}', 'input'), mode='wb') as file:
162+
file.write(replay.content)
163+
164+
165+
def download_option(days: list, year: int, cookies: dict, parts: list) -> None:
166+
for day in days:
167+
url = f'https://adventofcode.com/{year}/day/{day}'
168+
replay = requests.get(url, cookies=cookies)
169+
if replay.status_code == 200:
170+
try:
171+
os.mkdir(f'Day{day}')
172+
except FileExistsError:
173+
if 1 in parts:
174+
print(f'Folder \'Day{day}\' already exists. Ignoring')
175+
continue
176+
soup = BeautifulSoup(replay.content, features='html.parser')
177+
problems = soup.find_all('article', class_='day-desc')
178+
download_input(day, url, cookies)
179+
180+
part2_available = True
181+
if 2 in parts and len(problems) == 1 and cookies:
182+
print('You have not completed part one yet', file=sys.stderr)
183+
part2_available = False
184+
185+
for part in parts:
186+
if not part2_available and part == 2:
187+
break
188+
189+
md_content = md(fix_html_format(str(problems[part - 1])),
190+
heading_style='ATX',
191+
bullets='-+*',
192+
strong_em_symbol='**')
193+
194+
with open(os.path.join(os.getcwd(), f'Day{day}', 'README.md'), mode='at') as file:
195+
file.write(md_content)
196+
197+
else:
198+
print(f'Day {" " if day < 10 else ""}{day}: not found', file=sys.stderr)
199+
200+
201+
def main():
202+
current_year = datetime.date.today().year
203+
args, parser = parse_args(current_year)
204+
205+
if not len(sys.argv) > 1:
206+
parser.print_help()
207+
sys.exit(0)
208+
209+
if 'session_cookie' in args:
210+
set_session_option(args.session_cookie)
211+
else:
212+
days, year = get_dates(args, current_year)
213+
parts = get_parts_to_download(args.both_parts, args.part_2)
214+
cookies = get_session_cookie()
215+
if 2 in parts and not cookies:
216+
print('Error: can not download part two, cookie not set\nExiting')
217+
sys.exit(1)
218+
download_option(days, year, cookies, parts)
219+
220+
221+
if __name__ == '__main__':
222+
main()

0 commit comments

Comments
 (0)