-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5720597
Showing
42 changed files
with
2,609 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
use flake | ||
eval "$shellHook" | ||
|
||
VIRTUAL_ENV_PATH="${PWD}/.direnv/.venv" | ||
VIRTUAL_ENV_INSTALL_MARKER="${PWD}/.direnv/venv_install" | ||
|
||
if [[ ! -e "${VIRTUAL_ENV_INSTALL_MARKER}" ]]; then | ||
rm -rf "${VIRTUAL_ENV_PATH}" | ||
python -m venv "${VIRTUAL_ENV_PATH}" | ||
touch "${VIRTUAL_ENV_INSTALL_MARKER}" | ||
fi | ||
|
||
source "${VIRTUAL_ENV_PATH}/bin/activate" | ||
|
||
PIP_REQUIREMENTS_HASH=`cat requirements.txt | md5sum | cut -f 1 -d " "` | ||
PIP_INSTALL_MARKER="${PWD}/.direnv/pip-install-${PIP_REQUIREMENTS_HASH}" | ||
|
||
if [[ ! -e "${PIP_INSTALL_MARKER}" ]]; then | ||
pip install -r requirements.txt | ||
touch "${PIP_INSTALL_MARKER}" | ||
fi | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.direnv | ||
build | ||
out |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# Project Euler Offline | ||
|
||
Project Euler Offline is an unofficial compilation of the [Project Euler](https://projecteuler.net/) problem set for offline use. | ||
|
||
- It contains all Project Euler problems as of the date of compilation, including all problem data files as PDF attachments via the [attachfile2 LaTeX plugin](https://ctan.org/pkg/attachfile2). | ||
- Animations are produced with the [animate LaTeX plugin](https://ctan.org/pkg/animate). Note that animations require a PDF reader with JavaScript support. Support has been confirmed in [Okular](https://okular.kde.org/) on Linux (remember to select *View* → *Show Forms*). | ||
- Bonus feature: Appendix about Roman numerals. | ||
|
||
## Download | ||
|
||
Project Euler Offline is available in a compact and a spaced version. The spaced version renders problems on individual pages to leave room for note taking. | ||
|
||
- [Download compact version](https://github.com/pveierland/project_euler_offline/releases/latest/download/project_euler_offline.pdf) (Multiple problems per page) | ||
- [Download spaced version](https://github.com/pveierland/project_euler_offline/releases/latest/download/project_euler_offline_spaced.pdf) (Problems on individual pages) | ||
|
||
NB: Download size is ~14 MB. | ||
|
||
## Usage | ||
|
||
Download problem data: | ||
|
||
``` | ||
python -m project_euler_offline fetch | ||
``` | ||
|
||
Render compact version to PDF: | ||
|
||
``` | ||
python -m project_euler_offline render --pdf | ||
``` | ||
|
||
Render spaced version to PDF: | ||
``` | ||
python -m project_euler_offline render --pdf --spaced | ||
``` | ||
|
||
NB: Note that files are also downloaded during rendering. | ||
|
||
## Ideas | ||
|
||
- Consider separate screen and print versions to better utilize screen real estate. | ||
|
||
## License | ||
|
||
The original content of this repository is licensed under the [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) license](https://creativecommons.org/licenses/by-nc-sa/4.0/). Content within the `source_mods` folder are direct copies from Project Euler with necessary modifications for the compilation. |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
description = "Project Euler Offline"; | ||
|
||
inputs = { | ||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-21.11"; | ||
}; | ||
|
||
outputs = { self, nixpkgs }: | ||
let | ||
system = "x86_64-linux"; | ||
pkgs = (import nixpkgs { inherit system; }).pkgs; | ||
in | ||
{ | ||
devShell.${system} = pkgs.mkShell | ||
{ | ||
nativeBuildInputs = with pkgs; [ | ||
okular | ||
pandoc | ||
python39 | ||
python39Packages.beautifulsoup4 | ||
texlive.combined.scheme-full | ||
]; | ||
}; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"folders": [ | ||
{ | ||
"name": "project_euler_offline", | ||
"path": "." | ||
} | ||
], | ||
"settings": { | ||
"editor.codeActionsOnSave": { | ||
"source.fixAll.eslint": true, | ||
"source.organizeImports": true | ||
}, | ||
"editor.formatOnSave": true, | ||
"python.formatting.provider": "black", | ||
"python.languageServer": "Pylance" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from project_euler_offline.app import ProjectEulerOfflineApp | ||
|
||
ProjectEulerOfflineApp().run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
import argparse | ||
import asyncio | ||
import logging | ||
import subprocess | ||
from pathlib import Path | ||
|
||
import pydash | ||
from bs4 import BeautifulSoup | ||
from tqdm import tqdm | ||
|
||
from project_euler_offline.document_builder import DocumentBuilder | ||
from project_euler_offline.http_document_cache import ( | ||
HttpDocumentCache, | ||
MissingDataError, | ||
) | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class ProjectEulerOfflineApp: | ||
COMMANDS = ["fetch", "render"] | ||
|
||
def _retrieve_http_data(self, url_path, **kwargs): | ||
return asyncio.run( | ||
self._http_cache.retrieve_data(self._args.base_url + url_path, **kwargs) | ||
) | ||
|
||
def _write_http_resource(self, url_path, store_in_base=False, **kwargs): | ||
data = self._retrieve_http_data(url_path, **kwargs) | ||
path = self._output_path / ( | ||
Path(url_path).name if store_in_base else Path(url_path) | ||
) | ||
path.parent.mkdir(parents=True, exist_ok=True) | ||
path.write_bytes(data) | ||
return data, path | ||
|
||
def command_fetch(self): | ||
problem_ids = list(self.iterate_problem_ids()) | ||
|
||
if problem_ids: | ||
for problem_id in problem_ids: | ||
try: | ||
self.retrieve_problem_html( | ||
problem_id, | ||
cache_only=self._args.cache_only, | ||
force=self._args.force, | ||
) | ||
except MissingDataError: | ||
logger.error(f"failed to retrieve problem #{problem_id}") | ||
else: | ||
recent_problems_html = self._retrieve_http_data( | ||
"recent", cache_disable=True | ||
).decode("utf8") | ||
recent_problems_parsed = BeautifulSoup(recent_problems_html, "html.parser") | ||
recent_problem_id_tags = recent_problems_parsed.find( | ||
id="problems_table" | ||
).find_all(class_="id_column") | ||
|
||
if recent_problem_id_tags: | ||
latest_problem_id = max( | ||
int(recent_problem_id_tag.text) | ||
for recent_problem_id_tag in recent_problem_id_tags[1:] | ||
) | ||
|
||
for problem_id in tqdm( | ||
range(1, latest_problem_id + 1), desc="Fetching problem data..." | ||
): | ||
try: | ||
self._retrieve_http_data(f"problem={problem_id}") | ||
except MissingDataError: | ||
logger.error(f"failed to retrieve problem #{problem_id}") | ||
|
||
def command_render(self): | ||
document_builder = DocumentBuilder(is_spaced=self._args.spaced) | ||
|
||
problem_id = None | ||
problem_ids = list(self.iterate_problem_ids()) | ||
problem_count = len(problem_ids) if problem_ids else None | ||
|
||
explicit_problem_ids = bool(problem_ids) | ||
|
||
with tqdm(desc="Rendering problems...", total=problem_count) as progress_bar: | ||
while not explicit_problem_ids or problem_ids: | ||
if explicit_problem_ids: | ||
problem_id = problem_ids.pop(0) | ||
elif problem_id is None: | ||
problem_id = 1 | ||
else: | ||
problem_id += 1 | ||
|
||
source_mod_path = ( | ||
Path(__file__).parent / "../source_mods" / f"{problem_id}.tex" | ||
) | ||
|
||
if source_mod_path.exists(): | ||
source_mod_latex = source_mod_path.read_text() | ||
document_builder.append_problem_latex_content(source_mod_latex) | ||
else: | ||
# Intentionally only check cache, as we wish to receive None when there are no more problems: | ||
problem_data = self._retrieve_http_data( | ||
f"problem={problem_id}", cache_only=True | ||
) | ||
|
||
if not problem_data: | ||
break | ||
|
||
problem_html = problem_data.decode("utf8") | ||
document_builder.process_problem_html(problem_id, problem_html) | ||
|
||
progress_bar.update(1) | ||
|
||
for about_url_path in tqdm( | ||
document_builder._url_paths_about, "Rendering appendixes..." | ||
): | ||
source_mod_path = ( | ||
Path(__file__).parent | ||
/ "../source_mods" | ||
/ f"{pydash.snake_case(about_url_path)}.tex" | ||
) | ||
|
||
if source_mod_path.exists(): | ||
source_mod_latex = source_mod_path.read_text() | ||
document_builder.append_about_latex_content(source_mod_latex) | ||
else: | ||
about_html = self._retrieve_http_data( | ||
about_url_path, | ||
cache_only=self._args.cache_only, | ||
force=self._args.force, | ||
).decode("utf8") | ||
|
||
document_builder.process_about_html(about_url_path, about_html) | ||
|
||
animated_resources = [] | ||
|
||
for resource_url_path in tqdm( | ||
document_builder._url_paths_resources, "Processing resources..." | ||
): | ||
_, resource_file_path = self._write_http_resource( | ||
resource_url_path, | ||
store_in_base=False, | ||
cache_only=self._args.cache_only, | ||
force=self._args.force, | ||
) | ||
|
||
if resource_file_path.suffix == ".gif": | ||
gif_frame_count = int( | ||
subprocess.run( | ||
["identify", "-format", r"%n\n", str(resource_file_path)], | ||
capture_output=True, | ||
text=True, | ||
).stdout.splitlines()[0] | ||
) | ||
|
||
subprocess.run( | ||
[ | ||
"convert", | ||
"-coalesce", | ||
"-despeckle", | ||
str(resource_file_path), | ||
str(resource_file_path.with_suffix(".png")), | ||
] | ||
) | ||
|
||
animated_resources.append( | ||
dict( | ||
url_path=resource_url_path, | ||
file_path=resource_file_path.relative_to(self._output_path), | ||
frame_count=gif_frame_count, | ||
) | ||
) | ||
|
||
for embed_url_path in document_builder._url_paths_embedded: | ||
self._write_http_resource( | ||
embed_url_path, | ||
store_in_base=True, | ||
cache_only=self._args.cache_only, | ||
force=self._args.force, | ||
) | ||
|
||
document_builder.process_animated_resources(animated_resources) | ||
|
||
for source_file_path in list(Path(__file__).parent.glob("*.tex")) + list( | ||
Path(__file__).parent.glob("*.sty") | ||
): | ||
symlink_file_path = self._output_path / source_file_path.name | ||
|
||
if not symlink_file_path.exists(): | ||
symlink_file_path.symlink_to(source_file_path) | ||
|
||
build_name = "project_euler_offline" + ("_spaced" if self._args.spaced else "") | ||
output_latex_path = document_builder.write(self._output_path, build_name) | ||
|
||
if self._args.pdf: | ||
subprocess.run( | ||
[ | ||
"latexmk", | ||
"-pdf", | ||
str(output_latex_path.relative_to(self._output_path)), | ||
], | ||
cwd=str(self._output_path), | ||
) | ||
|
||
def iterate_problem_ids(self): | ||
if self._args.problems: | ||
for problem_group in self._args.problems.split(","): | ||
if "-" in problem_group: | ||
problem_start, problem_end = map( | ||
int, map(str.strip, problem_group.split("-")) | ||
) | ||
for problem_id in range(problem_start, problem_end + 1): | ||
yield problem_id | ||
else: | ||
yield int(problem_group) | ||
|
||
def run(self): | ||
parser = argparse.ArgumentParser( | ||
description="Project Euler Offline (Unofficial)" | ||
) | ||
parser.add_argument("--base_url", type=str, default="https://projecteuler.net/") | ||
parser.add_argument("--cache_only", action="store_true") | ||
parser.add_argument("--force", action="store_true") | ||
parser.add_argument("--output_path", type=str, default="out") | ||
parser.add_argument("--pdf", action="store_true") | ||
parser.add_argument("--problems", type=str) | ||
parser.add_argument("--spaced", action="store_true") | ||
parser.add_argument("command", choices=self.COMMANDS) | ||
|
||
self._args = parser.parse_args() | ||
self._output_path = Path(self._args.output_path) | ||
|
||
self._http_cache = HttpDocumentCache(self._output_path / "http_cache.sqlite3") | ||
|
||
if self._args.command == "fetch": | ||
self.command_fetch() | ||
elif self._args.command == "render": | ||
self.command_render() |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.