AdventOfCode Submissions Framework
Project description
aox - Python Advent of Code Submissions helper
AOX was created with the following ideals:
- I wanted to be able to run tests for each challenge
- I didn't want to write the same code to load the input, run the tests, and print the result
- I wanted all of my challenges to have the same structure
The main feature of AOX is auto-creating the boilerplate for each challenge,
Currently only Python>=3.7 is supported.
Quick start
Install aox
(Python>=3.7 needed):
pip install aox
Create your settings:
aox init-settings
Edit .aox/sensitive_user_settings.py
to put your AOC session token
# Copy-paste from your browser cookies
AOC_SESSION_ID = "..."
The following creates the boilerplate, and downloads your input:
aox add 2020 1 a
Edit the file to solve the challenge (your input is automatically passed
in), and return the solution inside the Challenge.solve
body:
class Challenge(BaseChallenge):
def solve(self, _input, debugger):
return your_awesome_calculations(_input)
If you want to check it works you can run it:
aox challenge 2020 1 a run
When you're happy with the result, submit it:
aox challenge 2020 1 a submit --yes
It will run your code, take the result, and submit it! If it's successful: hurray! Time to solve part B with:
aox add 2020 1 b
And so on
Features
Custom boilerplate
If you always want to import certain libraries (eg itertools
, functools
,
your own utils
library), you can create a template, so that you don't have to
write the same things again and again:
#!/usr/bin/env python3
import aox.challenge
import my_useful_utils
import itertools, functools
import re
class Challenge(aox.challenge.BaseChallenge):
...
...
Check the boilerplate customisation for more details.
Testing, running, debugging, interactive challenges
I've found that writing small doctests makes 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 run
aox challenge <year> <day> <part> test
.
It also prints your solution when you run
aox challenge <year> <day> <part> run
. If you add the --debug
flag, then you
can use the debugger
parameter to your Challenge.solve
method, which is
useful when you want to print diagnostic stats:
class Challenge(BaseChallenge):
def solve(self, _input, debugger):
# Print something, if debugging is enable, and enough
# time has passed from the previous print statement
debugger.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 flag
debugger.report_if(
"You won't see this")
# After enough time, `report_if` will print
time.sleep(5)
# To print something expensive, first check:
if debugger.should_report():
debugger.report(
"Some expensive calculation:", expensive_calculation())
# Debug through an iterable
for index in range(10):
debugger.step()
debugger.report_if(f"looking at index {index}")
...
# Also helpers to step through an iterable
for index in debugger.stepping(range(10)):
debugger.report_if(f"looking at index {index}")
...
# Or through a while loop
value = 0
while debugger.step_if(value < 3):
value += 1
debugger.report_if(f"checking value {value}")
...
return 6 * 7
To see more information check the Debugger documentation
aox challenge <year> <day> <part> run
will print:
Solution: 42 (in 0.0s)
While aox challenge <year> <day> <part> run --debug
will 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, eg
2019/15/A or 2019/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 a play
method
to your challenge, and run aox challenge <year> <day> <part> play
:
class Challenge(BaseChallenge):
def play(self):
_input = self.input
while True:
command = click.prompt("Enter u for up, d for down, q to quit")
if command == 'u':
...
By default, if you don't specify a mode, AOX will first test and then run your
solution with aox challenge <year> <day> <part>
:
4 tests in 1 modules passed in 0.02s
Solution: 1234567 (in 0.0s)
Debugging your code
Sometimes 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. The Debugger
class, that is passed in as debugger
in
the Challenge.solve
method, provides some very useful functionality:
if debugger:
orif debugger.enabled:
This allows you to only do something if you passed --debug
/-d
as an argument
debugger.step_count
,debugger.step_count_since_last_report
,debugger.step_frequency
, anddebugger.step_frequency_since_last_report
You 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 is
5s
, controlled by the --debug-interval
/-i
flag).
debugger.default_report()
anddebugger.default_report_if()
Similar to the above, but it uses the formatting function from the
DEFAULT_DEBUGGER_REPORT_FORMAT
to include some useful data in the output.
If your calculations run through nested function calls, you can also provide
additional info, by using debugger.extra_report_formats
, or using the
debugger.adding_extra_report_format
context manager:
def level_1(data, debugger):
def report_x(_, message):
return f"x: {x}"
# Will print: 'start x'
debugger.default_report("start x")
total = 0
for x in range(10000000):
with debugger.adding_extra_report_format(report_x):
# Will print: 'x: 0, start y'
debugger.default_report("start y")
total += level_2(data[x], debugger)
def level_2(data, debugger):
def report_y(_, message):
return f"y: {y}"
total = 0
for y in range(10000000):
with debugger.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)
return total
def level_3(data, debugger):
total = 0
for z in range(10000000):
total += data[z]['a'] * data[z]['b']
# Will print: 'x: 0, y: 0, z: 0'
debugger.default_report_if(f"z: {z}")
return total
debugger.duration_since_start
anddebugger.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 the pretty_*
property to get a nice 3h5m2s
rendition of time, or the get_pretty_*
methods to control how many digits
do
of precision do you want to include in the seconds eg 3h5m2s.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 iterable
for item in debugger.stepping(['a', 'b', 'c']):
...
# Step for each truthy value
value = 0
while debugger.step_if(value < 3):
value += 1
...
Reading your stars
To keep track how many and which stars you have, you can add your auth
credentials (check Session Cookie for details), and use
aox fetch
to refresh the stars cache. Then you can see them either via aox
or aox 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 stars
Or for a particular year with aox 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 swag
You 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 new
aox.summaries.BaseSummary
sub-class, and there are two default summaries
built-in:
Event summary
Simply add the following lines in your README:
[//]: # (event-summary-start)
[//]: # (event-summary-end)
And after every aox update-readme
you should see something like this:
Total | 2020 | 2019 | 2018 | 2017 |
---|---|---|---|---|
162 :star: | 50 :star: :star: | 47 :star: | 34 :star: :star: | 41 :star: :star: |
Submissions summary
Add the following lines in your README:
[//]: # (submissions-start)
[//]: # (submissions-end)
And after every aox update-readme
you should see something like this:
2020 | 2019 | |
---|---|---|
[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 summary
Simple override and register your summary class:
from aox.summary import BaseSummary, summary_registry
@summary_registry.register
class MyCustomSummary(BaseSummary):
# Don't forget to set your prefix
marker_prefix = "my-custom"
def generate(self, combined_info):
years_with_stars_or_code = [
year
for year, year_info in combined_info.year_infos.items()
if year_info.has_code or year_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 every aox update-readme
you 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:
Settings
After aox init-settings
, you'll have an .aox
folder in your repo:
.aox
├──.gitignore
├──sensitive_user_settings.py
├──site_data.json
└──user_settings.py
You should edit sensitive_user_settings.py
to 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 the Application
tab (eg in Chrome), and you should not
commit that file in git, as it contains your secrets.
site_data.json
is the cache of the AOC stars, from the last time you did
aox fetch
. If you want all the data that AOX uses you can use aox dump
.
Now, you can customise your experience in user_settings.py
:
Session Cookie
Simply load it from your not-git-commited file
AOC_SESSION_ID = sensitive_user_settings.AOC_SESSION_ID
Challenges code location
Where 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_root
CHALLENGES_MODULE_NAME_ROOT = None # Top module
If 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 prefix
Boilerplate customisation
By 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 python3
import aox.challenge
import my_useful_utils
import itertools, functools
import re
class Challenge(aox.challenge.BaseChallenge):
...
...
And then change the example part path:
from aox.boilerplate import DefaultBoilerplate
CHALLENGES_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-class
aox.boilerplate.BaseBoilerplate
, which AOX uses to know where to find your
code:
import aox.boilerplate
class MyCustomBoilerplate(aox.boilerplate.DefaultBoilerplate):
...
And use it:
CHALLENGES_BOILERPLATE = "my_custom_boilerplate.MyCustomBoilerplate"
Default format
To 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:
def verbose_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_format
If you want to customise it:
def custom_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_format
Site data cache
There is no reason to change this, and by default it lives in the .aox
folder.
In case you want it somewhere else (eg your home directory), you can replace
this (remembering to use Path
).
# Save this info in my home directory
SITE_DATA_PATH = Path.home().joinpath('aoc_site_data.json')
README location
If you want AOX to put a summary of your stars in markdown, you can use this to specify your README path
README_PATH = repo_root.joinpath('README.md')
Custom settings
If you want to keep some extra settings, you can add them to
.aox/user_settings.py
, and you can access them via the settings.module
attribute:
MY_CUSTOM_SETTING = "foo"
from aox.settings import settings_proxy
# Make sure to always use `settings_proxy()`
print(settings_proxy().module.MY_CUSTOM_SETTING)
Contributing:
Please see the relevant section in aox
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file aox-1.1.0.tar.gz
.
File metadata
- Download URL: aox-1.1.0.tar.gz
- Upload date:
- Size: 55.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.7.0 importlib_metadata/4.8.2 pkginfo/1.8.2 requests/2.22.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.8.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 94edce5f5d12c9a1845d7352815a01b8125892bd807c5ea8c39e46e37e214af9 |
|
MD5 | 7e343be9ebf6de134ece162340713f5a |
|
BLAKE2b-256 | 12939e1b668f1d5b8d4898c09f29eb0b8036ffbd10c10cb16376dd97dd7770d6 |
File details
Details for the file aox-1.1.0-py3-none-any.whl
.
File metadata
- Download URL: aox-1.1.0-py3-none-any.whl
- Upload date:
- Size: 61.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.7.0 importlib_metadata/4.8.2 pkginfo/1.8.2 requests/2.22.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.8.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 29254ae9de64d54aff44caac6ff200057fadb4251e864f3ffb3ddb36b1c909c0 |
|
MD5 | 5cf6313d8d8f26a066e37b17c9750fdf |
|
BLAKE2b-256 | 9ce6f37b0a62a160918ba4e25ee379d4624684996150f496a1e7964790f3a45b |