2020from 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
2631log = getLogger (__name__ )
@@ -38,6 +43,10 @@ class AocdError(Exception):
3843 pass
3944
4045
46+ class PuzzleUnsolvedError (AocdError ):
47+ pass
48+
49+
4150def 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+
91139def 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-
260282def 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" :
0 commit comments