Skip to content

Commit 5cb35da

Browse files
authored
Get answer (#9)
* add get_answer func * improve auto-submission * save bad answers too
1 parent f889e02 commit 5cb35da

File tree

5 files changed

+168
-53
lines changed

5 files changed

+168
-53
lines changed

aocd.py

Lines changed: 108 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@
2020
from termcolor import cprint
2121

2222

23-
__version__ = "0.6.0"
23+
__version__ = "0.7.0"
24+
__all__ = [
25+
"data", "get_data", "get_answer", "main", "submit", "__version__",
26+
"AocdError", "PuzzleUnsolvedError", "AOC_TZ", "current_day",
27+
"most_recent_year", "get_cookie",
28+
]
2429

2530

2631
log = getLogger(__name__)
@@ -38,6 +43,10 @@ class AocdError(Exception):
3843
pass
3944

4045

46+
class PuzzleUnsolvedError(AocdError):
47+
pass
48+
49+
4150
def get_data(session=None, day=None, year=None):
4251
"""
4352
Get data for day (1-25) and year (>= 2015)
@@ -61,7 +70,7 @@ def get_data(session=None, day=None, year=None):
6170
if err.errno != errno.ENOENT:
6271
raise
6372
else:
64-
log.info("reusing existing data %s", memo_fname)
73+
log.info("reusing existing data %s", memo_fname.replace(session, "<token>"))
6574
return data.rstrip("\r\n")
6675
log.info("getting data year=%s day=%s", year, day)
6776
t = time.time()
@@ -88,6 +97,45 @@ def get_data(session=None, day=None, year=None):
8897
return data.rstrip("\r\n")
8998

9099

100+
def get_answer(day, year, session=None, level=1):
101+
"""
102+
Get correct answer for day (1-25), year (>= 2015), and level (1 or 2)
103+
User's session cookie is needed (puzzle answers differ by user)
104+
Note: Answers are only revealed after a correct submission. If you
105+
have not already solved the puzzle, this AocdError will be raised.
106+
"""
107+
if session is None:
108+
session = get_cookie()
109+
memo_fname = MEMO_FNAME.format(session=session, year=year, day=day)
110+
part = {1: "a", 2: "b"}[int(level)]
111+
answer_fname = memo_fname.replace(".txt", "{}_answer.txt".format(part))
112+
if os.path.isfile(answer_fname):
113+
with open(answer_fname) as f:
114+
return f.read().strip()
115+
# check question page for already solved answers
116+
uri = URI.format(year=year, day=day)
117+
response = requests.get(
118+
uri,
119+
cookies={"session": session},
120+
headers={"User-Agent": USER_AGENT},
121+
)
122+
soup = bs4.BeautifulSoup(response.text, "html.parser")
123+
response.raise_for_status()
124+
paras = [p for p in soup.find_all('p') if p.text.startswith("Your puzzle answer was")]
125+
if paras:
126+
parta_correct_answer = paras[0].code.text
127+
save_correct_answer(answer=parta_correct_answer, day=day, year=year, level=1, session=session)
128+
if len(paras) > 1:
129+
_p1, p2 = paras
130+
partb_correct_answer = p2.code.text
131+
save_correct_answer(answer=partb_correct_answer, day=day, year=year, level=2, session=session)
132+
if os.path.isfile(answer_fname):
133+
with open(answer_fname) as f:
134+
return f.read().strip()
135+
msg = "Answer {}-{}{} is not available".format(year, day, part)
136+
raise PuzzleUnsolvedError(msg)
137+
138+
91139
def most_recent_year():
92140
"""
93141
This year, if it's December.
@@ -231,32 +279,6 @@ def get_day_and_year():
231279
raise AocdError("Failed introspection of day")
232280

233281

234-
def user_has_completed_part_a(day, year, session):
235-
memo_fname = MEMO_FNAME.format(session=session, year=year, day=day)
236-
part_a_answer_fname = memo_fname.replace(".txt", "a_answer.txt")
237-
if os.path.isfile(part_a_answer_fname):
238-
return True
239-
# check question page for already solved answer
240-
uri = URI.format(year=year, day=day)
241-
response = requests.get(
242-
uri,
243-
cookies={"session": session},
244-
headers={"User-Agent": USER_AGENT},
245-
)
246-
soup = bs4.BeautifulSoup(response.text, "html.parser")
247-
response.raise_for_status()
248-
paras = [p for p in soup.find_all('p') if p.text.startswith("Your puzzle answer was")]
249-
if paras:
250-
parta_correct_answer = paras[0].code.text
251-
save_correct_answer(answer=parta_correct_answer, day=day, year=year, level=1, session=session)
252-
if len(paras) > 1:
253-
_p1, p2 = paras
254-
partb_correct_answer = p2.code.text
255-
save_correct_answer(answer=partb_correct_answer, day=day, year=year, level=2, session=session)
256-
return True
257-
return False
258-
259-
260282
def save_correct_answer(answer, day, year, level, session):
261283
memo_fname = MEMO_FNAME.format(session=session, year=year, day=day)
262284
part = {"1": "a", "2": "b"}[str(level)]
@@ -267,7 +289,30 @@ def save_correct_answer(answer, day, year, level, session):
267289
f.write(str(answer).strip())
268290

269291

270-
def submit(answer, level=None, day=None, year=None, session=None, reopen=True):
292+
def save_incorrect_answer(answer, day, year, level, session, extra):
293+
memo_fname = MEMO_FNAME.format(session=session, year=year, day=day)
294+
part = {"1": "a", "2": "b"}[str(level)]
295+
answer_fname = memo_fname.replace(".txt", "{}_bad_answers.txt".format(part))
296+
_ensure_intermediate_dirs(answer_fname)
297+
with open(answer_fname, "a") as f:
298+
log.info("caching this data")
299+
f.write(str(answer).strip() + " " + extra.replace("\n", " ") + "\n")
300+
301+
302+
def get_incorrect_answers(day, year, level, session):
303+
memo_fname = MEMO_FNAME.format(session=session, year=year, day=day)
304+
part = {"1": "a", "2": "b"}[str(level)]
305+
answer_fname = memo_fname.replace(".txt", "{}_bad_answers.txt".format(part))
306+
result = {}
307+
if os.path.isfile(answer_fname):
308+
with open(answer_fname) as f:
309+
for line in f:
310+
answer, _sep, extra = line.strip().partition(" ")
311+
result[answer] = extra
312+
return result
313+
314+
315+
def submit(answer, level=None, day=None, year=None, session=None, reopen=True, quiet=False):
271316
if level not in {1, 2, "1", "2", None}:
272317
raise AocdError("level must be 1 or 2")
273318
if session is None:
@@ -277,13 +322,22 @@ def submit(answer, level=None, day=None, year=None, session=None, reopen=True):
277322
if year is None:
278323
year = most_recent_year()
279324
if level is None:
280-
# figure out if user is submitting for part a or part b
281-
if user_has_completed_part_a(day, year, session):
282-
log.debug("you already completed part a, submitting for part b")
283-
level = 2
284-
else:
325+
# guess if user is submitting for part a or part b
326+
try:
327+
get_answer(day=day, year=year, session=session, level=1)
328+
except PuzzleUnsolvedError:
285329
log.debug("submitting for part a")
286330
level = 1
331+
else:
332+
log.debug("submitting for part b (part a is already completed)")
333+
level = 2
334+
bad_guesses = get_incorrect_answers(day=day, year=year, level=level, session=session)
335+
if str(answer) in bad_guesses:
336+
if not quiet:
337+
msg = "aocd will not submit that answer again. You've previously guessed {} and the server responded:"
338+
print(msg.format(answer))
339+
cprint(bad_guesses[str(answer)], "red")
340+
return
287341
uri = URI.format(year=year, day=day) + "/answer"
288342
log.info("posting to %s", uri)
289343
response = requests.post(
@@ -309,10 +363,24 @@ def submit(answer, level=None, day=None, year=None, session=None, reopen=True):
309363
elif "That's not the right answer" in message:
310364
color = "red"
311365
you_guessed = soup.article.span.code.text
312-
log.debug("wrong answer %s", you_guessed)
366+
log.warning("wrong answer %s", you_guessed)
367+
save_incorrect_answer(answer=answer, day=day, year=year, level=level, session=session, extra=soup.article.text)
313368
elif "You gave an answer too recently" in message:
314-
color = "red"
315-
cprint(soup.article.text, color=color)
369+
wait_pattern = r"You have (?:(\d+)m )?(\d+)s left to wait"
370+
try:
371+
[(minutes, seconds)] = re.findall(wait_pattern, message)
372+
except ValueError:
373+
log.warning(message)
374+
color = "red"
375+
else:
376+
wait_time = int(seconds)
377+
if minutes:
378+
wait_time += 60 * int(minutes)
379+
log.info("Waiting %d seconds to autoretry", wait_time)
380+
time.sleep(wait_time)
381+
return submit(answer=answer, level=level, day=day, year=year, session=session, reopen=reopen, quiet=quiet)
382+
if not quiet:
383+
cprint(message, color=color)
316384
return response
317385

318386

@@ -324,14 +392,14 @@ def main():
324392
nargs="?",
325393
type=int,
326394
choices=range(1,26),
327-
default=min(aoc_now.day, 25),
395+
default=min(aoc_now.day, 25) if aoc_now.month == 12 else 1,
328396
help="1-25 (default: %(default)s)",
329397
)
330398
parser.add_argument(
331399
"year",
332400
nargs="?",
333401
type=int,
334-
choices=range(2015, aoc_now.year + 1),
402+
choices=range(2015, aoc_now.year + int(aoc_now.month == 12)),
335403
default=most_recent_year(),
336404
help=">= 2015 (default: %(default)s)",
337405
)
@@ -344,10 +412,7 @@ class Aocd(object):
344412
_module = sys.modules[__name__]
345413

346414
def __dir__(self):
347-
return [
348-
"data", "get_data", "main", "submit", "get_day_and_year", "get_cookie",
349-
"AocdError", "__version__", "current_day", "most_recent_year",
350-
]
415+
return __all__
351416

352417
def __getattr__(self, name):
353418
if name == "data":

tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import pytest
22

33

4+
@pytest.fixture(autouse=True)
5+
def mocked_sleep(mocker):
6+
no_sleep_till_brooklyn = mocker.patch("aocd._module.time.sleep")
7+
return no_sleep_till_brooklyn
8+
9+
410
@pytest.fixture(autouse=True)
511
def remove_user_env(tmpdir, monkeypatch):
612
token_file = tmpdir / ".config/aocd/token"

tests/test_get_answer.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import aocd
2+
3+
4+
def test_get_answer(tmpdir):
5+
saved = tmpdir / ".config/aocd/thetesttoken/2017/13b_answer.txt"
6+
saved.ensure(file=True)
7+
saved.write("the answer")
8+
answer = aocd.get_answer(day=13, year=2017, level=2)
9+
assert answer == "the answer"

tests/test_get_data.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@
66
from aocd import AocdError
77

88

9-
@pytest.fixture(autouse=True)
10-
def mocked_sleep(mocker):
11-
no_sleep_till_brooklyn = mocker.patch("aocd._module.time.sleep")
12-
return no_sleep_till_brooklyn
13-
14-
159
def test_get_from_server(requests_mock):
1610
mock = requests_mock.get(
1711
url="https://adventofcode.com/2018/day/1/input",
@@ -77,7 +71,7 @@ def test_aocd_user_agent_in_req_headers(requests_mock):
7771
aocd.get_data(year=2018, day=1)
7872
assert mock.call_count == 1
7973
headers = mock.last_request._request.headers
80-
assert headers["User-Agent"] == "aocd.py/v0.6.0"
74+
assert headers["User-Agent"] == "aocd.py/v{}".format(aocd.__version__)
8175

8276

8377
def test_data_is_cached_from_successful_request(tmpdir, requests_mock):

tests/test_submit.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,43 @@ def test_submit_when_already_solved(requests_mock, capsys):
6363
assert msg in out
6464

6565

66-
def test_submit_when_submitted_too_recently(requests_mock, capsys):
67-
html = '''<article><p>You gave an answer too recently; you have to wait after submitting an answer before trying again. You have 30s left to wait. <a href="/2015/day/25">[Return to Day 25]</a></p></article>'''
66+
def test_submit_when_submitted_too_recently_and_autoretry(requests_mock, capsys, mocked_sleep):
67+
html1 = '''<article><p>You gave an answer too recently; you have to wait after submitting an answer before trying again. You have 30s left to wait. <a href="/2015/day/25">[Return to Day 25]</a></p></article>'''
68+
html2 = "<article>That's the right answer. Yeah!!</article>"
69+
requests_mock.post(
70+
"https://adventofcode.com/2015/day/25/answer",
71+
[{"text": html1}, {"text": html2}],
72+
)
73+
submit(1234, level=1, year=2015, day=25, reopen=False)
74+
mocked_sleep.assert_called_once_with(30)
75+
out, err = capsys.readouterr()
76+
msg = "That's the right answer. Yeah!!"
77+
msg = colored(msg, "green")
78+
assert msg in out
79+
80+
81+
def test_submit_when_submitted_too_recently_and_autoretry_and_quiet(requests_mock, capsys, mocked_sleep):
82+
html1 = '''<article><p>You gave an answer too recently; you have to wait after submitting an answer before trying again. You have 3m 30s left to wait. <a href="/2015/day/25">[Return to Day 25]</a></p></article>'''
83+
html2 = "<article>That's the right answer. Yeah!!</article>"
84+
requests_mock.post(
85+
"https://adventofcode.com/2015/day/25/answer",
86+
[{"text": html1}, {"text": html2}],
87+
)
88+
submit(1234, level=1, year=2015, day=25, reopen=False, quiet=True)
89+
mocked_sleep.assert_called_once_with(3*60 + 30)
90+
out, err = capsys.readouterr()
91+
assert out == err == ""
92+
93+
94+
def test_submit_when_submitted_too_recently_no_autoretry(requests_mock, capsys):
95+
html = '''<article><p>You gave an answer too recently</p></article>'''
6896
requests_mock.post(
6997
url="https://adventofcode.com/2015/day/25/answer",
7098
text=html,
7199
)
72100
submit(1234, level=1, year=2015, day=25, reopen=False)
73101
out, err = capsys.readouterr()
74-
msg = "You gave an answer too recently; you have to wait after submitting an answer before trying again. You have 30s left to wait. [Return to Day 25]"
102+
msg = "You gave an answer too recently"
75103
msg = colored(msg, "red")
76104
assert msg in out
77105

@@ -203,3 +231,16 @@ def test_failure_to_create_dirs_unhandled(mocker):
203231
err.errno = errno.EEXIST
204232
mocker.patch("aocd._module.os.makedirs", side_effect=[TypeError, err])
205233
aocd._module._ensure_intermediate_dirs("/")
234+
235+
236+
def test_cannot_submit_same_bad_answer_twice(requests_mock, capsys):
237+
mock = requests_mock.post(
238+
url="https://adventofcode.com/2015/day/1/answer",
239+
text="<article><p>That's not the right answer. (You guessed <span><code>69</code>.)</span></a></p></article>",
240+
)
241+
submit(year=2015, day=1, level=1, answer=69)
242+
submit(year=2015, day=1, level=1, answer=69)
243+
submit(year=2015, day=1, level=1, answer=69, quiet=True)
244+
assert mock.call_count == 1
245+
out, err = capsys.readouterr()
246+
assert "aocd will not submit that answer again" in out

0 commit comments

Comments
 (0)