Пример #1
0
 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),
         )
Пример #2
0
 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))
Пример #3
0
    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)
Пример #4
0
 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)
Пример #5
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)
Пример #7
0
    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
Пример #8
0
    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)
Пример #10
0
    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
Пример #11
0
    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)
Пример #12
0
 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
Пример #13
0
 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
Пример #14
0
    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
Пример #15
0
 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}",
         ]))
Пример #16
0
 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}")
Пример #17
0
    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
Пример #18
0
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)
Пример #19
0
    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
Пример #20
0
    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)
Пример #21
0
def replacing_settings(new_settings):
    old_settings = settings_proxy(False)
    settings_proxy.set(new_settings)
    yield settings_proxy(False)
    settings_proxy.set(old_settings)
Пример #22
0
 def get_input_filename(self):
     return settings_proxy().challenges_boilerplate\
         .get_day_input_filename(self.year, self.day)
Пример #23
0
 def relative_path(self):
     return self.path.relative_to(settings_proxy().challenges_root)
Пример #24
0
 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()
Пример #25
0
 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()
Пример #26
0
    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)
Пример #27
0
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