From 4f4fe1eff2bd431eea9031da7882162f5f3ca259 Mon Sep 17 00:00:00 2001 From: Connor Rhodes Date: Fri, 24 Apr 2026 08:52:01 -0500 Subject: [PATCH] change structure --- dictations_viewer/templates/index.html | 368 ------------------------- print/app.py | 110 -------- print/templates/index.html | 82 ------ print/tests/__init__.py | 0 print/tests/conftest.py | 59 ---- print/tests/test_pdf_utils.py | 13 - print/tests/test_routes.py | 69 ----- print/uploads/.gitkeep | 0 pyproject.toml | 5 + server.py | 95 +++++-- uv.lock | 51 ++++ 11 files changed, 126 insertions(+), 726 deletions(-) delete mode 100644 dictations_viewer/templates/index.html delete mode 100644 print/app.py delete mode 100644 print/templates/index.html delete mode 100644 print/tests/__init__.py delete mode 100644 print/tests/conftest.py delete mode 100644 print/tests/test_pdf_utils.py delete mode 100644 print/tests/test_routes.py delete mode 100644 print/uploads/.gitkeep diff --git a/dictations_viewer/templates/index.html b/dictations_viewer/templates/index.html deleted file mode 100644 index 8685b9f..0000000 --- a/dictations_viewer/templates/index.html +++ /dev/null @@ -1,368 +0,0 @@ - - - - - - Dictations Viewer - - - - - - - -
-
-

Dictations

-

Recent voice notes

-
- -
-
-
- - - - - diff --git a/print/app.py b/print/app.py deleted file mode 100644 index 8a97cb5..0000000 --- a/print/app.py +++ /dev/null @@ -1,110 +0,0 @@ -import os -import subprocess -from pathlib import Path - -import pypdf -from flask import Flask, flash, redirect, render_template, request, url_for -from werkzeug.utils import secure_filename - -UPLOAD_FOLDER = str(Path(__file__).parent / "uploads") -ALLOWED_EXTENSIONS = {"pdf"} -PRINTER_NAME = "HL-2270DW_series" -MAX_PAGES = 10 - -app = Flask(__name__) -app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER -app.config["SECRET_KEY"] = os.urandom(24) -app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 - - -def allowed_file(filename): - return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS - - -def get_pdf_page_count(filepath): - try: - with open(filepath, "rb") as f: - reader = pypdf.PdfReader(f) - return len(reader.pages) - except Exception as e: - app.logger.error(f"Could not read PDF pages: {e}") - return None - - -@app.route("/", methods=["GET", "POST"]) -def upload_and_print(): - if request.method == "POST": - if "file" not in request.files: - flash("No file part in the request.", "error") - return redirect(request.url) - - file = request.files["file"] - - if file.filename == "": - flash("No file selected. Please choose a PDF to print.", "error") - return redirect(request.url) - - if file and allowed_file(file.filename): - filename = secure_filename(file.filename) - os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True) - filepath = os.path.join(app.config["UPLOAD_FOLDER"], filename) - file.save(filepath) - - try: - page_count = get_pdf_page_count(filepath) - if page_count is None: - flash( - "Could not process the PDF file. It might be corrupt.", "error" - ) - return redirect(request.url) - - if page_count > MAX_PAGES: - flash( - f"Error: File has {page_count} pages. The maximum allowed is {MAX_PAGES}.", - "error", - ) - return redirect(request.url) - - app.logger.info( - f"Sending '{filename}' ({page_count} pages) to printer '{PRINTER_NAME}'" - ) - command = [ - "lp", - "-d", - PRINTER_NAME, - filepath, - ] - result = subprocess.run( - command, capture_output=True, text=True, check=True - ) - app.logger.info(f"lp command stdout: {result.stdout}") - flash(f"Success! '{filename}' has been sent to the printer.", "success") - - except subprocess.CalledProcessError as e: - app.logger.error(f"Error printing file: {e}") - app.logger.error(f"lp command stderr: {e.stderr}") - flash( - "Error sending file to printer. The printer may be offline or misconfigured.", - "error", - ) - - except Exception as e: - app.logger.error(f"An unexpected error occurred: {e}") - flash("An unexpected server error occurred.", "error") - - finally: - if os.path.exists(filepath): - os.remove(filepath) - app.logger.info(f"Cleaned up temporary file: {filepath}") - - return redirect(url_for("upload_and_print")) - - else: - flash("Invalid file type. Please upload a PDF file.", "error") - return redirect(request.url) - - return render_template("index.html", printer_name=PRINTER_NAME, max_pages=MAX_PAGES) - - -if __name__ == "__main__": - app.run(host="127.0.0.1", port=57928, debug=False) diff --git a/print/templates/index.html b/print/templates/index.html deleted file mode 100644 index fb1aea0..0000000 --- a/print/templates/index.html +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - Web Print Service - - - - - - - - -
-
- -
- -

PDF Web Print

-

Upload a PDF to print to {{ printer_name }}.

-

(Max {{ max_pages }} pages)

-
- - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
- {% for category, message in messages %} - - {% endfor %} -
- {% endif %} - {% endwith %} - -
-
- - -
- -
- -
-
-
- -
-

A simple Flask web app for local printing.

-
-
- - - - - diff --git a/print/tests/__init__.py b/print/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/print/tests/conftest.py b/print/tests/conftest.py deleted file mode 100644 index 441d010..0000000 --- a/print/tests/conftest.py +++ /dev/null @@ -1,59 +0,0 @@ -import io -import tempfile - -import pypdf -import pytest - -from app import app as flask_app, get_pdf_page_count - - -@pytest.fixture -def app(): - flask_app.config["TESTING"] = True - flask_app.config["UPLOAD_FOLDER"] = tempfile.mkdtemp() - flask_app.config["SECRET_KEY"] = "test-secret" - yield flask_app - - -@pytest.fixture -def client(app): - return app.test_client() - - -def _make_pdf(num_pages=3): - writer = pypdf.PdfWriter() - for _ in range(num_pages): - writer.add_blank_page(width=612, height=792) - buf = io.BytesIO() - writer.write(buf) - buf.seek(0) - return buf - - -@pytest.fixture -def sample_pdf(): - return _make_pdf(3).read() - - -@pytest.fixture -def large_pdf(): - return _make_pdf(15).read() - - -@pytest.fixture -def corrupt_file(): - return b"This is not a PDF at all" - - -@pytest.fixture -def sample_pdf_path(tmp_path): - path = tmp_path / "test.pdf" - path.write_bytes(_make_pdf(3).read()) - return str(path) - - -@pytest.fixture -def corrupt_file_path(tmp_path): - path = tmp_path / "corrupt.pdf" - path.write_bytes(b"not a pdf") - return str(path) diff --git a/print/tests/test_pdf_utils.py b/print/tests/test_pdf_utils.py deleted file mode 100644 index 7054f51..0000000 --- a/print/tests/test_pdf_utils.py +++ /dev/null @@ -1,13 +0,0 @@ -from app import get_pdf_page_count - - -def test_page_count_valid_pdf(sample_pdf_path): - assert get_pdf_page_count(sample_pdf_path) == 3 - - -def test_page_count_corrupt_file(corrupt_file_path): - assert get_pdf_page_count(corrupt_file_path) is None - - -def test_page_count_nonexistent_file(): - assert get_pdf_page_count("/nonexistent/file.pdf") is None diff --git a/print/tests/test_routes.py b/print/tests/test_routes.py deleted file mode 100644 index 4414a63..0000000 --- a/print/tests/test_routes.py +++ /dev/null @@ -1,69 +0,0 @@ -import io -import os -from unittest.mock import patch - - -def test_get_index(client): - resp = client.get("/") - assert resp.status_code == 200 - assert b"printer" in resp.data.lower() or b"upload" in resp.data.lower() - - -def test_post_no_file(client): - resp = client.post("/", data={}, follow_redirects=True) - assert resp.status_code == 200 - assert b"No file part" in resp.data - - -def test_post_empty_filename(client): - resp = client.post("/", data={"file": (io.BytesIO(b""), "")}, follow_redirects=True) - assert resp.status_code == 200 - assert b"No file selected" in resp.data - - -def test_post_invalid_extension(client): - data = {"file": (io.BytesIO(b"hello"), "document.txt")} - resp = client.post("/", data=data, follow_redirects=True) - assert resp.status_code == 200 - assert b"Invalid file type" in resp.data - - -@patch("app.subprocess.run") -def test_post_valid_pdf(mock_run, client, app, sample_pdf): - mock_run.return_value.stdout = "request-id-123" - mock_run.return_value.stderr = "" - mock_run.return_value.returncode = 0 - - data = {"file": (io.BytesIO(sample_pdf), "test.pdf")} - resp = client.post("/", data=data, follow_redirects=True) - assert resp.status_code == 200 - assert b"Success" in resp.data - - upload_dir = app.config["UPLOAD_FOLDER"] - remaining = os.listdir(upload_dir) - assert len(remaining) == 0 - - mock_run.assert_called_once() - cmd = mock_run.call_args[0][0] - assert cmd[0] == "lp" - - -@patch("app.subprocess.run") -def test_post_pdf_exceeds_max_pages(mock_run, client, large_pdf): - data = {"file": (io.BytesIO(large_pdf), "big.pdf")} - resp = client.post("/", data=data, follow_redirects=True) - assert resp.status_code == 200 - assert b"maximum allowed" in resp.data.lower() - - mock_run.assert_not_called() - - -def test_post_corrupt_pdf(client, app, corrupt_file): - data = {"file": (io.BytesIO(corrupt_file), "bad.pdf")} - resp = client.post("/", data=data, follow_redirects=True) - assert resp.status_code == 200 - assert b"corrupt" in resp.data.lower() - - upload_dir = app.config["UPLOAD_FOLDER"] - remaining = os.listdir(upload_dir) - assert len(remaining) == 0 diff --git a/print/uploads/.gitkeep b/print/uploads/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/pyproject.toml b/pyproject.toml index d4abdb1..d55a5a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,3 +8,8 @@ dependencies = [ "pymongo>=4.0", "pypdf>=4.0", ] + +[dependency-groups] +dev = [ + "pytest>=9.0.3", +] diff --git a/server.py b/server.py index 480a720..0d4ad0f 100644 --- a/server.py +++ b/server.py @@ -6,24 +6,52 @@ from flask import Flask, render_template_string from werkzeug.middleware.dispatcher import DispatcherMiddleware from werkzeug.serving import run_simple +CATEGORIES = ["personal", "home", "work"] + root = Flask(__name__) +def _discover_apps(): + apps = [] + base = Path(__file__).parent + for category in CATEGORIES: + cat_dir = base / category + if not cat_dir.is_dir(): + continue + for app_dir in sorted(cat_dir.iterdir()): + if app_dir.is_dir() and (app_dir / "app.py").exists(): + apps.append((category, app_dir.name)) + return apps + + @root.route("/") def index(): - apps = [ - d.name - for d in Path(__file__).parent.iterdir() - if d.is_dir() and (d / "app.py").exists() - ] - items = "".join( - f'' - f'\u203a' - f'
{a.replace("_", " ").replace("-", " ").title()}
' - f'
/{a}
' - f"
" - for i, a in enumerate(sorted(apps)) - ) + apps = _discover_apps() + grouped: dict[str, list[tuple[str, str]]] = {} + for category, app_name in apps: + grouped.setdefault(category, []).append((category, app_name)) + + sections = "" + delay = 0 + for category in CATEGORIES: + if category not in grouped: + continue + items_html = "" + for cat, name in grouped[category]: + items_html += ( + f'' + f'\u203a' + f'
{name.replace("_", " ").replace("-", " ").title()}
' + f'
/{cat}/{name}
' + f"
" + ) + delay += 1 + sections += f""" +
+

{category.title()}

+
{items_html}
+
""" + return render_template_string(f""" @@ -86,6 +114,20 @@ def index(): color: var(--text-muted); }} + .category {{ + margin-bottom: 28px; + }} + + .category-title {{ + font-size: 0.82rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 10px; + padding-left: 4px; + }} + .app-list {{ display: flex; flex-direction: column; @@ -154,7 +196,7 @@ def index():

Tools

Flask applications

-
{items}
+ {sections} @@ -164,18 +206,21 @@ def index(): def load_sub_apps(): mounts = {} base = Path(__file__).parent - for app_dir in sorted(base.iterdir()): - app_file = app_dir / "app.py" - if not app_dir.is_dir() or not app_file.exists(): + for category in CATEGORIES: + cat_dir = base / category + if not cat_dir.is_dir(): continue - spec = importlib.util.spec_from_file_location(app_dir.name, app_file) - mod = importlib.util.module_from_spec(spec) - sys.modules[app_dir.name] = ( - mod # must be registered before exec so Flask(__name__) resolves root_path correctly - ) - spec.loader.exec_module(mod) - mounts[f"/{app_dir.name}"] = mod.app - print(f" mounted /{app_dir.name}") + for app_dir in sorted(cat_dir.iterdir()): + app_file = app_dir / "app.py" + if not app_dir.is_dir() or not app_file.exists(): + continue + module_name = f"{category}.{app_dir.name}" + spec = importlib.util.spec_from_file_location(module_name, app_file) + mod = importlib.util.module_from_spec(spec) + sys.modules[module_name] = mod + spec.loader.exec_module(mod) + mounts[f"/{category}/{app_dir.name}"] = mod.app + print(f" mounted /{category}/{app_dir.name}") return mounts diff --git a/uv.lock b/uv.lock index 9e73450..4041e50 100644 --- a/uv.lock +++ b/uv.lock @@ -70,6 +70,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/c8/8aaf447698c4d59aa853fd318eed300b5c9e44459f242ab8ead6c9c09792/gunicorn-25.3.0-py3-none-any.whl", hash = "sha256:cacea387dab08cd6776501621c295a904fe8e3b7aae9a1a3cbb26f4e7ed54660", size = 208403, upload-time = "2026-03-27T00:00:27.386Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "itsdangerous" version = "2.2.0" @@ -176,6 +185,11 @@ dependencies = [ { name = "pypdf" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "flask", specifier = ">=3.0" }, @@ -184,6 +198,9 @@ requires-dist = [ { name = "pypdf", specifier = ">=4.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=9.0.3" }] + [[package]] name = "packaging" version = "26.1" @@ -193,6 +210,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + [[package]] name = "pymongo" version = "4.17.0" @@ -263,6 +298,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/d6/1d5c60cc17bbdf37c1552d9c03862fc6d32c5836732a0415b2d637edc2d0/pypdf-6.10.2-py3-none-any.whl", hash = "sha256:aa53be9826655b51c96741e5d7983ca224d898ac0a77896e64636810517624aa", size = 336308, upload-time = "2026-04-15T16:37:34.851Z" }, ] +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + [[package]] name = "werkzeug" version = "3.1.8"