def get_combined_info(self): for part in self.parts_to_create_on_disk: settings_proxy().challenges_boilerplate.create_part(*part) return CombinedInfo\ .from_repo_and_account_infos( RepoInfo.from_roots(), AccountInfo.from_collected_data(self.collected_data), )
def test_init_settings_with_no_settings(self): with preparing_to_init_settings() as settings_directory: self.assertTrue(Controller().init_settings()) self.assertTrue(settings_proxy.has()) self.assertTrue(settings_proxy().path.exists()) if sys.version_info >= (3, 9): self.assertTrue( settings_proxy().path.is_relative_to(settings_directory))
def from_part(cls, part, day_info, existing_files=None): path = settings_proxy().challenges_boilerplate\ .get_part_filename(day_info.year, day_info.day, part) module_name = settings_proxy().challenges_boilerplate\ .get_part_module_name(day_info.year, day_info.day, part) if existing_files is None: has_code = path.exists() else: has_code = path in existing_files return cls(day_info, part, has_code, path, module_name)
def test_init_settings_with_existing_settings_doesnt_recreate_it(self): with preparing_to_init_settings() as settings_directory: Controller().init_settings() existing_settings = settings_proxy() with patch.object(Controller, 'create_settings') \ as create_settings_mock: self.assertFalse(Controller().init_settings()) self.assertTrue(settings_proxy.has()) self.assertTrue(settings_proxy().path.exists()) if sys.version_info >= (3, 9): self.assertTrue( settings_proxy().path.is_relative_to(settings_directory)) self.assertEqual(settings_proxy(), existing_settings) self.assertEqual(create_settings_mock.call_count, 0)
def creating_parts_on_disk(parts): with tempfile.TemporaryDirectory(dir=str(get_root_directory())) \ as challenges_root: challenges_module_name_root = Path(challenges_root).name extra_settings = { "challenges_root": Path(challenges_root), "challenges_boilerplate": DefaultBoilerplate(), "challenges_module_name_root": challenges_module_name_root, } with amending_settings(**extra_settings), \ resetting_modules(challenges_module_name_root): for part in parts: settings_proxy().challenges_boilerplate.create_part(*part) yield challenges_root
def test_fetching_account_info_writes_to_missing_cache(self): account_info = AccountInfo.from_collected_data({ "username": "******", "total_stars": 3, "years": { 2020: { "year": 2020, "stars": 3, "days": { 1: 2, 2: 1, 3: 0, } }, }, }) with self.preparing_to_fetch_info(account_info) as controller: site_data_path = settings_proxy().site_data_path site_data_path.unlink() self.assertTrue(controller.fetch_account_info()) self.assertTrue(site_data_path.exists()) self.assertEqual(json.loads(site_data_path.read_text()), account_info.serialise()) self.assertTrue( controller.combined_info.get_part(2020, 2, 'a').has_star) self.assertFalse( controller.combined_info.get_part(2020, 3, 'a').has_star)
def update_readme(self): """ Update README with summaries, presumably because code or stars were added. """ readme_path = settings_proxy().readme_path if not readme_path: return False if not self.combined_info.has_site_data: click.echo(f"Since {e_error('local site data')} are missing the " f"README {e_error('cannot be updated')}: run " f"{e_suggest('aox fetch')} first") return False readme_text = readme_path.read_text() updated_readme_text = summary_registry.update_text( readme_text, self.combined_info) if updated_readme_text == readme_text: click.echo(f"No need to update {e_success('README')}") return False readme_path.write_text(updated_readme_text) click.echo(f"Updated {e_success('README')} with site data") return True
def refresh_challenge_input(self, year, day, only_if_empty=True): """Refresh a challenge's input from the AOC website""" input_path = settings_proxy().challenges_boilerplate\ .get_day_input_filename(year, day) if only_if_empty and input_path.exists() and input_path.lstat( ).st_size: return False _input = WebAoc().get_input_page(year, day) if not _input: click.echo( f"Could not update input for {e_error(f'{year} {day}')}") return False if input_path.exists() and _input == input_path.read_text(): click.echo(f"Input did not change for {e_warn(f'{year} {day}')} " f"({e_value(f'{len(_input)} bytes')})") return False input_path.write_text(_input) click.echo(f"Updated input for {e_success(f'{year} {day}')} " f"({e_value(f'{len(_input)} bytes')}) at " f"{e_value(str(input_path))}") return True
def test_fetching_empty_account_info_doesnt_change_combined_info_and_cached_data( self): with self.preparing_to_fetch_info(None) as controller: combined_info = controller.combined_info self.assertFalse(controller.fetch_account_info()) self.assertEqual(settings_proxy().site_data_path.read_text(), "null\n") self.assertEqual(controller.combined_info, combined_info)
def fetch_account_info(self): """Refresh the stars from the AOC website""" account_info = AccountInfo.from_site() if account_info is None: click.echo(f"Could {e_error('not fetch data')}") return False if settings_proxy().site_data_path: with settings_proxy().site_data_path.open('w') as f: json.dump(account_info.serialise(), f, indent=2) self.update_combined_info(account_info=account_info) click.echo(f"Fetched data for {e_success(account_info.username)}: " f"{e_star(f'{str(account_info.total_stars)} stars')} in " f"{e_success(str(len(account_info.year_infos)))} years") return True
def from_cache(cls): site_data_path = settings_proxy().site_data_path if not site_data_path or not site_data_path.exists(): return None with site_data_path.open() as f: serialised = json.load(f) return cls.deserialise(serialised)
def from_year(cls, year, repo_info, existing_files=None): year_info = cls( repo_info=repo_info, year=year, has_code=False, path=settings_proxy().challenges_boilerplate .get_year_directory(year), ) year_info.fill(existing_files) return year_info
def from_day(cls, day, year_info, existing_files=None): day_info = cls( year_info=year_info, day=day, has_code=False, path=settings_proxy().challenges_boilerplate.get_day_directory( year_info.year, day), ) day_info.fill(existing_files) return day_info
def apply_report_formats(self, message: str = "") -> str: """Apply the default, and any extra report formats, to a message""" report_formats = self.extra_report_formats for report_format in reversed(report_formats): message = report_format(self, message) default_debugger_report_format = settings_proxy()\ .default_debugger_report_format if default_debugger_report_format: message = default_debugger_report_format(self, message) return message
def get_part_module_name(self, year, day, part): """ >>> DefaultBoilerplate().get_part_module_name(2020, 5, 'a') 'year_2020.day_05.part_a' >>> DefaultBoilerplate().get_part_module_name(2020, 15, 'a') 'year_2020.day_15.part_a' """ return ".".join( filter(None, [ settings_proxy().challenges_module_name_root, f"year_{year}.day_{day:0>2}.part_{part}", ]))
def get_year_directory(self, year: int, relative: bool = False): """ >>> str(DefaultBoilerplate().get_year_directory(2020, True)) 'year_2020' """ if relative: base = Path() else: base = settings_proxy().challenges_root if base is None: return None return base.joinpath(f"year_{year}")
def init_settings(self, settings_directory=None): """Create a new settings directory for the user, if they're missing""" if settings_proxy.has() and settings_proxy().path.exists(): click.echo( f"User settings {e_warn('already exist')} at " f"{e_value(str(settings_proxy().path))}. Will not overwrite " f"them.") self.reload_combined_info() return False self.create_settings(settings_directory=settings_directory) self.reload_combined_info() return True
def amending_settings(**kwargs): settings_dict = { key: value for key, value in settings_proxy().__dict__.items() if key in kwargs } settings_proxy().__dict__.update(**kwargs) yield settings_proxy() settings_proxy().__dict__.update(settings_dict)
def add_challenge(self, year: int, day: int, part: str): """Add challenge code boilerplate, if it's not already there""" if not settings_proxy().challenges_boilerplate\ .create_part(year, day, part): return False self.refresh_challenge_input(year=year, day=day) part_filename = self.combined_info.get_part(year, day, part).path click.echo( f"Added challenge {e_success(f'{year} {day} {part.upper()}')} at " f"{e_value(str(part_filename))}") self.show_challenge_urls(year, day) self.update_combined_info(repo_info=RepoInfo.from_roots()) return True
def check_readme(self, parts_to_create_on_disk, collected_data, initial_content, expected_content, expected_result): if initial_content is None: readme_file = nullcontext() new_settings = {"readme_path": None} else: readme_file = tempfile.NamedTemporaryFile(mode="w") new_settings = {"readme_path": Path(readme_file.name)} with using_controller(parts_to_create_on_disk, collected_data) \ as (controller, _, _), readme_file, \ amending_settings(**new_settings): if initial_content is not None: readme_file.write(initial_content) readme_file.flush() self.assertEqual(controller.update_readme(), expected_result) readme_path = settings_proxy().readme_path if readme_path and readme_path.exists(): content = readme_path.read_text() else: content = None self.assertEqual(content, expected_content)
def replacing_settings(new_settings): old_settings = settings_proxy(False) settings_proxy.set(new_settings) yield settings_proxy(False) settings_proxy.set(old_settings)
def get_input_filename(self): return settings_proxy().challenges_boilerplate\ .get_day_input_filename(self.year, self.day)
def relative_path(self): return self.path.relative_to(settings_proxy().challenges_root)
def __init__(self): self.module = self.get_module() self.year, self.day, self.part = settings_proxy()\ .challenges_boilerplate\ .extract_from_filename(self.module.__file__) self.input = self.get_input()
def get_input(self): """Get the input for the challenge""" return settings_proxy().challenges_boilerplate\ .get_day_input_filename(self.year, self.day)\ .read_text()
def test_some_parts_exist(self): with tempfile.TemporaryDirectory() as challenges_root: with amending_settings( challenges_root=Path(challenges_root), challenges_boilerplate=DefaultBoilerplate()): settings_proxy().challenges_boilerplate.create_part( 2020, 1, 'b') settings_proxy().challenges_boilerplate.create_part( 2020, 2, 'b') settings_proxy().challenges_boilerplate.create_part( 2020, 3, 'b') settings_proxy().challenges_boilerplate.create_part( 2020, 10, 'b') settings_proxy().challenges_boilerplate.create_part( 2020, 11, 'a') settings_proxy().challenges_boilerplate.create_part( 2019, 1, 'b') settings_proxy().challenges_boilerplate.create_part( 2019, 3, 'a') settings_proxy().challenges_boilerplate.create_part( 2019, 11, 'b') folder_contents = glob.glob(f"{challenges_root}/**/*", recursive=True) repo_info = RepoInfo.from_roots() self.assertTrue(repo_info.has_code) self.assertTrue( set(repo_info.year_infos).issuperset( {2015, 2016, 2017, 2018, 2019, 2020})) # Check years with code self.assertEqual( { year_info.year for year_info in repo_info.year_infos.values() if str(year_info.path) in folder_contents }, {2019, 2020}) self.assertEqual( { year_info.year for year_info in repo_info.year_infos.values() if year_info.has_code }, {2019, 2020}) self.assertEqual( { year_info.year for year_info in repo_info.year_infos.values() if year_info.path.exists() }, {2019, 2020}) # Check days with code self.assertEqual( {(day_info.year, day_info.day) for year_info in repo_info.year_infos.values() for day_info in year_info.day_infos.values() if str(day_info.path) in folder_contents}, { (2020, 1), (2020, 2), (2020, 3), (2020, 10), (2020, 11), (2019, 1), (2019, 3), (2019, 11), }) self.assertEqual( {(day_info.year, day_info.day) for year_info in repo_info.year_infos.values() for day_info in year_info.day_infos.values() if day_info.has_code}, { (2020, 1), (2020, 2), (2020, 3), (2020, 10), (2020, 11), (2019, 1), (2019, 3), (2019, 11), }) self.assertEqual( {(day_info.year, day_info.day) for year_info in repo_info.year_infos.values() for day_info in year_info.day_infos.values() if day_info.path.exists()}, { (2020, 1), (2020, 2), (2020, 3), (2020, 10), (2020, 11), (2019, 1), (2019, 3), (2019, 11), }) # Check parts with code self.assertEqual( {(part_info.year, part_info.day, part_info.part) for year_info in repo_info.year_infos.values() for day_info in year_info.day_infos.values() for part_info in day_info.part_infos.values() if str(part_info.path) in folder_contents}, { (2020, 1, 'a'), (2020, 1, 'b'), (2020, 2, 'a'), (2020, 2, 'b'), (2020, 3, 'a'), (2020, 3, 'b'), (2020, 10, 'a'), (2020, 10, 'b'), (2020, 11, 'a'), (2019, 1, 'a'), (2019, 1, 'b'), (2019, 3, 'a'), (2019, 11, 'a'), (2019, 11, 'b'), }) self.assertEqual( {(part_info.year, part_info.day, part_info.part) for year_info in repo_info.year_infos.values() for day_info in year_info.day_infos.values() for part_info in day_info.part_infos.values() if part_info.has_code}, { (2020, 1, 'a'), (2020, 1, 'b'), (2020, 2, 'a'), (2020, 2, 'b'), (2020, 3, 'a'), (2020, 3, 'b'), (2020, 10, 'a'), (2020, 10, 'b'), (2020, 11, 'a'), (2019, 1, 'a'), (2019, 1, 'b'), (2019, 3, 'a'), (2019, 11, 'a'), (2019, 11, 'b'), }) self.assertEqual( {(part_info.year, part_info.day, part_info.part) for year_info in repo_info.year_infos.values() for day_info in year_info.day_infos.values() for part_info in day_info.part_infos.values() if part_info.path.exists()}, { (2020, 1, 'a'), (2020, 1, 'b'), (2020, 2, 'a'), (2020, 2, 'b'), (2020, 3, 'a'), (2020, 3, 'b'), (2020, 10, 'a'), (2020, 10, 'b'), (2020, 11, 'a'), (2019, 1, 'a'), (2019, 1, 'b'), (2019, 3, 'a'), (2019, 11, 'a'), (2019, 11, 'b'), }) for year, year_info in repo_info.year_infos.items(): self.assertEqual(year_info.year, year) self.assertEqual(set(year_info.day_infos), set(range(1, 26))) for day, day_info in year_info.day_infos.items(): self.assertEqual(day_info.year_info, year_info) self.assertEqual(day_info.year, year) self.assertEqual(day_info.day, day) self.assertEqual(set(day_info.part_infos), {'a', 'b'}) for part, part_info in day_info.part_infos.items(): self.assertEqual(part_info.day_info, day_info) self.assertEqual(part_info.year, year) self.assertEqual(part_info.day, day) self.assertEqual(part_info.part, part)
class WebAoc: """ Helper class that abstracts requests to the AOC site. The session ID is necessary before any request. """ session_id: Optional[str] = field( default_factory=lambda: settings_proxy().aoc_session_id) root_url = 'https://adventofcode.com' headers = { "User-Agent": "aox", } cookies = {} def get_events_url(self): """ >>> WebAoc('test-session').get_events_url() 'https://adventofcode.com/events' """ return f"{self.root_url}/events" def get_year_url(self, year): """ >>> WebAoc('test-session').get_year_url(2020) 'https://adventofcode.com/2020' """ return f"{self.root_url}/{year}" def get_day_url(self, year, day): """ >>> WebAoc('test-session').get_day_url(2020, 5) 'https://adventofcode.com/2020/day/5' """ return f"{self.root_url}/{year}/day/{day}" def get_input_url(self, year, day): """ >>> WebAoc('test-session').get_input_url(2020, 5) 'https://adventofcode.com/2020/day/5/input' """ return f"{self.root_url}/{year}/day/{day}/input" def get_answer_url(self, year, day): """ >>> WebAoc('test-session').get_answer_url(2020, 5) 'https://adventofcode.com/2020/day/5/answer' """ return f"{self.root_url}/{year}/day/{day}/answer" def is_configured(self): """ >>> WebAoc('test-session').is_configured() True >>> WebAoc(None).is_configured() False >>> WebAoc('').is_configured() False """ return bool(self.session_id) def get_events_page(self): """Get the page with stars per year""" return self.get_html(self.get_events_url(), 'events information') def get_year_page(self, year): """Get the page with stars per day""" return self.get_html( self.get_year_url(year), f"year {year} information") def get_input_page(self, year, day): """Get the input for a particular day""" return self.get_text( self.get_input_url(year, day), f"year {year} day {day} input") def submit_solution(self, year, day, part, solution): """Post a solution""" return self.post_html( self.get_answer_url(year, day), { "level": 1 if part == "a" else 2, "answer": solution, }, f"year {year} day {day} input") def get_html(self, url, parse_name, *args, **kwargs): """Get parsed HTML""" return self.get( url=url, parse_type='html', parse_name=parse_name, *args, **kwargs) def get_text(self, url, parse_name, *args, **kwargs): """Get raw text""" return self.get( url=url, parse_type='text', parse_name=parse_name, *args, **kwargs) def get(self, *args, **kwargs): """Get a page""" return self.fetch(requests.get, *args, **kwargs) def post(self, *args, **kwargs): """Submit a request""" return self.fetch(requests.post, *args, **kwargs) def post_html(self, url, data, parse_name, *args, **kwargs): """Post and return parsed HTML""" return self.post( url=url, data=data, parse_type='html', parse_name=parse_name, *args, **kwargs) def fetch(self, method, url, extra_headers=None, extra_cookies=None, parse_type=None, parse_name=None, data=None): """ Generic method for interacting with the site. It can also parse the the result as a particular type (eg HTML). """ if not self.is_configured(): return None if not url.startswith(self.root_url): raise Exception( f"Only AOC URLs can be accessed (starting with " f"'{self.root_url}'), not '{url}'") response = method( url, data=data, headers=self.get_headers(extra_headers), cookies=self.get_cookies(extra_cookies), ) if parse_type: return self.parse(response, parse_type, parse_name) return response def get_headers(self, extra_headers=None): """ Construct the headers for a request >>> WebAoc('test-session').get_headers() {'User-Agent': 'aox'} >>> WebAoc('test-session').get_headers({}) {'User-Agent': 'aox'} >>> WebAoc('test-session').get_headers({'User-Agent': 'test'}) {'User-Agent': 'test'} >>> WebAoc('test-session').get_headers( ... {'User-Agent': 'test', 'foo': 'bar'}) {'User-Agent': 'test', 'foo': 'bar'} """ return { **self.headers, **(extra_headers or {}), } def get_cookies(self, extra_cookies=None): """ Construct the cookies for a request >>> WebAoc('test-session').get_cookies() {'session': 'test-session'} >>> WebAoc('test-session').get_cookies({}) {'session': 'test-session'} >>> WebAoc('test-session').get_cookies({'session': 'other'}) {'session': 'other'} >>> WebAoc('test-session').get_cookies( ... {'session': 'other', 'foo': 'bar'}) {'session': 'other', 'foo': 'bar'} """ return { "session": self.session_id, **self.cookies, **(extra_cookies or {}), } def parse(self, response, _type, name): """ Parse a response as a particular type (eg HTML) >>> from requests import Response >>> _response = Response() >>> _response._content = b'<html><body><article>Hi' >>> _response.status_code = 200 >>> html = WebAoc('test-session').parse(_response, 'html', 'test') >>> html <html><body><article>Hi</article></body></html> >>> html.article <article>Hi</article> >>> _response = Response() >>> _response._content = b'Hello there' >>> _response.status_code = 200 >>> WebAoc('test-session').parse(_response, 'text', 'test') 'Hello there' """ if _type == 'html': return self.as_html(response, name) if _type == 'text': return self.as_text(response, name) else: raise Exception(f"Unknown parse type '{_type}'") def as_html(self, response, name): """ Parse a response as HTML >>> from requests import Response >>> _response = Response() >>> _response._content = b'<html><body><article>Hi' >>> _response.status_code = 200 >>> html = WebAoc('test-session').as_html(_response, 'test') >>> html <html><body><article>Hi</article></body></html> >>> html.article <article>Hi</article> >>> _response = Response() >>> _response._content = b'Oops' >>> _response.status_code = 400 >>> WebAoc('test-session').as_html(_response, 'test') """ if not response: return None if not response.ok: click.echo( f"Could not get {e_error(name)} from the AOC site " f"({response.status_code}) - is the internet down, AOC down, " f"the URL is wrong, or are you banned?") return None return bs4.BeautifulSoup(response.text, "html.parser") def as_text(self, response, name): """ Parse a response as text >>> from requests import Response >>> _response = Response() >>> _response._content = b'Hello there' >>> _response.status_code = 200 >>> WebAoc('test-session').as_text(_response, 'test') 'Hello there' >>> _response = Response() >>> _response._content = b'Oops' >>> _response.status_code = 400 >>> WebAoc('test-session').as_text(_response, 'test') """ if not response: return None if not response.ok: click.echo( f"Could not get {e_error(name)} from the AOC site " f"({response.status_code}) - is the internet down, AOC down, " f"the URL is wrong, or are you banned?") return None return response.text