Package: aox
aox - Python Advent of Code Submissions helperAOX was created with the following ideals:I wanted to be able to run tests for each challengeI didn't want to write the same code to load the input, run the tests, and
print the resultI wanted all of my challenges to have the same structureThe main feature of AOX is auto-creating the boilerplate for each challenge,Currently only Python>=3.7 is supported.Quick startInstallaox(Python>=3.7 needed):pipinstallaoxCreate your settings:aoxinit-settingsEdit.aox/sensitive_user_settings.pyto put your AOC session token# Copy-paste from your browser cookiesAOC_SESSION_ID="..."The following creates the boilerplate, and downloads your input:aoxadd20201aEdit the file to solve the challenge (your input is automatically passed
in), and return the solution inside theChallenge.solvebody:classChallenge(BaseChallenge):defsolve(self,_input,debugger):returnyour_awesome_calculations(_input)If you want to check it works you can run it:aoxchallenge20201arunWhen you're happy with the result, submit it:aoxchallenge20201asubmit--yesIt will run your code, take the result, and submit it! If it's successful:
hurray! Time to solve part B with:aoxadd20201bAnd so onFeaturesCustom boilerplateIf you always want to import certain libraries (egitertools,functools,
your ownutilslibrary), you can create a template, so that you don't have to
write the same things again and again:#!/usr/bin/env python3importaox.challengeimportmy_useful_utilsimportitertools,functoolsimportreclassChallenge(aox.challenge.BaseChallenge):......Check theboilerplate customisationfor more
details.Testing, running, debugging, interactive challengesI've found that writing smalldoctestsmakes it much easier to develop
solutions for AOC, especially when they become more complicated.AOX automatically picks up all doctests in a challenge and runs them if you runaox challenge <year> <day> <part> test.It also prints your solution when you runaox challenge <year> <day> <part> run. If you add the--debugflag, then you
can use thedebuggerparameter to yourChallenge.solvemethod, which is
useful when you want to print diagnostic stats:classChallenge(BaseChallenge):defsolve(self,_input,debugger):# Print something, if debugging is enable, and enough# time has passed from the previous print statementdebugger.report_if("Many many lines of debug information")# By default there is a 5s interval between printing# To change that, use the --debug-interval/-i flagdebugger.report_if("You won't see this")# After enough time, `report_if` will printtime.sleep(5)# To print something expensive, first check:ifdebugger.should_report():debugger.report("Some expensive calculation:",expensive_calculation())# Debug through an iterableforindexinrange(10):debugger.step()debugger.report_if(f"looking at index{index}")...# Also helpers to step through an iterableforindexindebugger.stepping(range(10)):debugger.report_if(f"looking at index{index}")...# Or through a while loopvalue=0whiledebugger.step_if(value<3):value+=1debugger.report_if(f"checking value{value}")...return6*7To see more information check theDebugger documentationaox challenge <year> <day> <part> runwill print:Solution: 42 (in 0.0s)Whileaox challenge <year> <day> <part> run --debugwill print:Many many lines of debug information
That you don't want to read every time
Solution: 42 (in 0.0s)Some challenges require you to interact with a program you're emulating, eg2019/15/Aor2019/25/A, so before you write an automated solution, you might
elect to add an interactive mode. Other times, it's useful to be able to
interactively explore the problem space. To do that, simply add aplaymethod
to your challenge, and runaox challenge <year> <day> <part> play:classChallenge(BaseChallenge):defplay(self):_input=self.inputwhileTrue:command=click.prompt("Enter u for up, d for down, q to quit")ifcommand=='u':...By default, if you don't specify a mode, AOX will first test and then run your
solution withaox challenge <year> <day> <part>:4testsin1modulespassedin0.02sSolution:1234567(in0.0s)Debugging your codeSometimes you need to see some details about the process you're running, and
that could mean printing some info in the console, especially if you have a very
inefficient algorithm. TheDebuggerclass, that is passed in asdebuggerin
theChallenge.solvemethod, provides some very useful functionality:if debugger:orif debugger.enabled:This allows you to only do something if you passed--debug/-das an argumentdebugger.step_count,debugger.step_count_since_last_report,debugger.step_frequency, anddebugger.step_frequency_since_last_reportYou inspect how many steps total/per second have you performed since the
start/last time you reported.if debugger.should_report(): debugger.report(...)ordebugger.report_if(...)Advance the step count once, and print something to the console, if the debugger
is enabled, and enough time has passed since the last reporting (default is5s, controlled by the--debug-interval/-iflag).debugger.default_report()anddebugger.default_report_if()Similar to the above, but it uses the formatting function from theDEFAULT_DEBUGGER_REPORT_FORMATto include some useful data in the output.If your calculations run through nested function calls, you can also provide
additional info, by usingdebugger.extra_report_formats, or using thedebugger.adding_extra_report_formatcontext manager:deflevel_1(data,debugger):defreport_x(_,message):returnf"x:{x}"# Will print: 'start x'debugger.default_report("start x")total=0forxinrange(10000000):withdebugger.adding_extra_report_format(report_x):# Will print: 'x: 0, start y'debugger.default_report("start y")total+=level_2(data[x],debugger)deflevel_2(data,debugger):defreport_y(_,message):returnf"y:{y}"total=0foryinrange(10000000):withdebugger.adding_extra_report_format(report_y):# Will print: 'x: 0, y: 0, start z'debugger.default_report("start z")total+=level_3(data[y],debugger)returntotaldeflevel_3(data,debugger):total=0forzinrange(10000000):total+=data[z]['a']*data[z]['b']# Will print: 'x: 0, y: 0, z: 0'debugger.default_report_if(f"z:{z}")returntotaldebugger.duration_since_startanddebugger.duration_since_last_report, as
well asdebugger.pretty_duration_*anddebugger.get_pretty_duration_*()You can get the duration since the start/last report, which would be the number
of seconds since then. Since these are fine precision floats, you would like to
use thepretty_*property to get a nice3h5m2srendition of time, or theget_pretty_*methods to control how manydigitsdo
of precision do you want to include in the seconds eg3h5m2s.23.debugger.step(),debugger.stepping(), anddebugger.step_if()It signifies that you have performed a number of steps (by default 1).There are two helper methods:# Step for each item in the passed in iterableforitemindebugger.stepping(['a','b','c']):...# Step for each truthy valuevalue=0whiledebugger.step_if(value<3):value+=1...Reading your starsTo keep track how many and which stars you have, you can add your auth
credentials (checkSession Cookiefor details), and useaox fetchto refresh the stars cache. Then you can see them either viaaoxoraox list:Found 4 years with code and 162 stars:
  * 2020: 25 days with code and 50 stars
  * 2019: 25 days with code and 47 stars
  * 2018: 25 days with code and 34 stars
  * 2017: 25 days with code and 41 starsOr for a particular year withaox list 2019:Found 25 days with code in 2019 with 47 stars:
  * 25*!, 24**, 23**, 22*x, 21**, 20**, 19**, 18**, 17**, 16*x, 15**, 14**, 13**, 12**, 11**, 10**, 9**, 8**, 7**, 6**, 5**, 4**, 3**, 2**, 1**Displaying your swagYou might be proud of how many stars you've gotten over the years, or you might
want to keep track of which challenges do you still have to solve. In any case,
AOX provides the ability to display star & code summaries in your README
automatically.The way you can display these are customisable, by creating a newaox.summaries.BaseSummarysub-class, and there are two default summaries
built-in:Event summarySimply add the following lines in your README:[//]:# (event-summary-start)[//]:# (event-summary-end)And after everyaox update-readmeyou should see something like this:Total2020201920182017162 :star:50 :star: :star:47 :star:34 :star: :star:41 :star: :star:Submissions summaryAdd the following lines in your README:[//]:# (submissions-start)[//]:# (submissions-end)And after everyaox update-readmeyou should see something like this:20202019[Code][co-20]    &             [Challenges][ch-20][Code][co-19]    &                         [Challenges][ch-19]50 :star: :star:47 :star: / 2 :x: / 1 :grey_exclamation:1[Code][co-20-01] :star: :star: [Challenge][ch-20-01][Code][co-19-01] :star: :star:             [Challenge][ch-19-01]2[Code][co-20-02] :star: :star: [Challenge][ch-20-02][Code][co-19-02] :star: :star:             [Challenge][ch-19-02].........24[Code][co-20-24] :star: :star: [Challenge][ch-20-24][Code][co-19-24] :star: :star:             [Challenge][ch-19-24]25[Code][co-20-25] :star: :star: [Challenge][ch-20-25][Code][co-19-25] :star: :grey_exclamation: [Challenge][ch-19-25]Your custom summarySimple override and register your summary class:fromaox.summaryimportBaseSummary,summary_registry@summary_registry.registerclassMyCustomSummary(BaseSummary):# Don't forget to set your prefixmarker_prefix="my-custom"defgenerate(self,combined_info):years_with_stars_or_code=[yearforyear,year_infoincombined_info.year_infos.items()ifyear_info.has_codeoryear_info.stars]return("Your custom markdown using stars & code info:\n"f"You have stars or code in{len(years_with_stars_or_code)}years\n"f"In 2020 you had{combined_info.year_infos[2020].stars}:star:\n")Don't forget to import your module, which can easily be done in your settings:EXTRA_MODULE_IMPORTS=['my_custom_summary']And to add the following lines in your README:[//]:# (my-custom-start)[//]:# (my-custom-end)And after everyaox update-readmeyou should see something like this:Your custom markdown using stars & code info:
You have stars or code in 2 years
In 2020 you had 50 :star:SettingsAfteraox init-settings, you'll have an.aoxfolder in your repo:.aox
├──.gitignore
├──sensitive_user_settings.py
├──site_data.json
└──user_settings.pyYou should editsensitive_user_settings.pyto put the authentication cookie
for your account, to allow AOX access your AOC stats:AOC_SESSION_ID=("a-very-long-hex-string""that-you-can-get-from-your-browser")You can find it in theApplicationtab (eg in Chrome), and you should not
commit that file in git, as it contains your secrets.site_data.jsonis the cache of the AOC stars, from the last time you didaox fetch. If you want all the data that AOX uses you can useaox dump.Now, you can customise your experience inuser_settings.py:Session CookieSimply load it from your not-git-commited fileAOC_SESSION_ID=sensitive_user_settings.AOC_SESSION_IDChallenges code locationWhere do your challenges live? The default boilerplate structure is<repo-root>/year_<year>/day_<day>/part_<part>.py, so you don't have to change
this:CHALLENGES_ROOT=repo_rootCHALLENGES_MODULE_NAME_ROOT=None# Top moduleIf you want to use a different folder, eg<repo-root>/my/challenges/year_<year>/day_<day>/part_<part>.py, you can do:CHALLENGES_ROOT=repo_root.joinpath('my','challenges')CHALLENGES_MODULE_NAME_ROOT='my.challenges'# Module prefixBoilerplate customisationBy default, boilerplate is structure as mentioned above:CHALLENGES_BOILERPLATE="aox.boilerplate.DefaultBoilerplate"The most common use case is to create a template file with eg custom imports:#!/usr/bin/env python3importaox.challengeimportmy_useful_utilsimportitertools,functoolsimportreclassChallenge(aox.challenge.BaseChallenge):......And then change the example part path:fromaox.boilerplateimportDefaultBoilerplateCHALLENGES_BOILERPLATE=DefaultBoilerplate(example_part_path=repo_root.joinpath('my_custom_example_part.py'),)Or for more complex customisation, eg if you want a different structure (eg<repo-root>/<year>_<day>_<part>.py), you can sub-classaox.boilerplate.BaseBoilerplate, which AOX uses to know where to find your
code:importaox.boilerplateclassMyCustomBoilerplate(aox.boilerplate.DefaultBoilerplate):...And use it:CHALLENGES_BOILERPLATE="my_custom_boilerplate.MyCustomBoilerplate"Default formatTo avoid repeating yourself every time you want to print something out, you can
define (or use the default) formatter function to add some standard details:defverbose_debugger_format(debugger:'Debugger',message:str)->str:return(f"Step:{debugger.step_count},{message}, time: "f"{debugger.pretty_duration_since_start}, total steps/s: "f"{debugger.step_frequency}, recent steps/s: "f"{debugger.step_frequency_since_last_report}")DEFAULT_DEBUGGER_REPORT_FORMAT=verbose_debugger_formatIf you want to customise it:defcustom_debugger_format(debugger:'Debugger',message:str)->str:return(f"Custom format, delta time is "f"{debugger.pretty_duration_since_last_report}for{message}")DEFAULT_DEBUGGER_REPORT_FORMAT=custom_debugger_formatSite data cacheThere is no reason to change this, and by default it lives in the.aoxfolder.
In case you want it somewhere else (eg your home directory), you can replace
this (remembering to usePath).# Save this info in my home directorySITE_DATA_PATH=Path.home().joinpath('aoc_site_data.json')README locationIf you want AOX to put a summary of your stars in markdown, you can use this to
specify your README pathREADME_PATH=repo_root.joinpath('README.md')Custom settingsIf you want to keep some extra settings, you can add them to.aox/user_settings.py, and you can access them via thesettings.moduleattribute:MY_CUSTOM_SETTING="foo"fromaox.settingsimportsettings_proxy# Make sure to always use `settings_proxy()`print(settings_proxy().module.MY_CUSTOM_SETTING)Contributing:Please see the relevant section inaox