@@ -1,13 +1,4 @@.env.git.gitignore.editorconfig.eslintrc.playwright-mcp__pycache__docker-compose.ymlDockerfileLICENSE.mdREADME.mdnode_modulesstatic/target/frontend/node_modules/.git/dist
@@ -1,20 +0,0 @@root = true[*]charset = utf-8indent_style = spaceindent_size = 2end_of_line = lfinsert_final_newline = truetrim_trailing_whitespace = true[*.py]indent_size = 4[*.md]trim_trailing_whitespace = falseindent_size = 4[Makefile]indent_style = tabindent_size = 4
@@ -1,5 +1,4 @@.env.venv__pycache__node_modulesstatic/target/dist/frontend/node_modules
@@ -4,37 +4,70 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co## What This IsA personal blog (blog.bythewood.me) built as a Flask app that renders markdown files. No database — blog posts are `.md` files in `content/posts/` with YAML frontmatter. Uses WeasyPrint for PDF export and Mistune for markdown rendering.A personal blog (blog.bythewood.me) built as a Rust axum app that renders markdown files. No database — blog posts are `.md` files in `content/posts/` with YAML frontmatter. Uses headless Chromium for PDF export and comrak for markdown rendering.This is a Rust port of the original Flask version (now deleted). Performance: ~30-50x lower memory, ~10-20x higher RPS, sub-millisecond per-request latency in release mode.## Commands- **Dev server:** `make run` (runs Vite watch + Flask dev server concurrently)- **Build frontend:** `make build` (or `bun run build`)- **Dev server:** `make run` (runs Vite watch + cargo run concurrently on port 8000)- **Production build:** `make build` (Vite assets + release binary)- **Run release binary:** `make start`- **Bench sweep:** `make bench` (oha load test, used to compare against the old Flask version when both run side-by-side)- **Docker build:** `sudo docker build .`There are no tests or linters configured.## Architecture**Backend:** Single-file Flask app (`app.py`). Posts are loaded from `content/posts/*.md` at startup (reloaded per-request in debug mode). Each post has frontmatter fields: title, slug, date, publish_date, tags, description, cover_image. Posts with `publish_date` in the future are hidden.**Backend:** Single-binary axum app (`src/main.rs`). Posts are loaded from `content/posts/*.md` once at startup. Each post has frontmatter fields: title, slug, date, publish_date, tags, description, cover_image. Posts with `publish_date` in the future are hidden.**Frontend pipeline:** Vite (run from `frontend/`) builds `frontend/static_src/` → `dist/`. Entry point is `frontend/static_src/index.js` which imports SCSS and JS. Output filenames are content-hashed (`base-[hash].js`, `base-[hash].css`) and a Vite manifest (`dist/.vite/manifest.json`) is read at runtime so templates can resolve the hashed names. Uses Bootstrap 5, CodeMirror, and Monaspace Argon font.**Templates:** Jinja2 templates in `templates/` rendered by minijinja (Jinja2-faithful Rust engine by Armin Ronacher). `base.html` is the layout. Markdown post content is rendered through comrak with a custom renderer (`src/markdown.rs`) that wraps blocks in `div.block-*` classes — mirrors the original Mistune renderer pattern.**PDF generation:** `src/pdf.rs` spawns chrome-headless-shell with `--print-to-pdf` against a temp `.html` file (the `.html` suffix matters — without it Chromium renders the HTML as plain text). Chromium binary is located via env var `CHROMIUM_BIN`, then PATH search, then a `/opt/playwright-browsers/` glob fallback.**Frontend pipeline:** Vite builds `static_src/` → `static/`. Entry point is `static_src/index.js` which imports SCSS and JS. Output filenames are content-hashed (`base-[hash].js`, `base-[hash].css`) and a Vite manifest (`static/.vite/manifest.json`) is read at runtime so templates can resolve the hashed names for cache busting. Uses Bootstrap 5, CodeMirror (syntax highlighting in posts), and Monaspace Argon font.**Manifest reload:** `templates.rs::build_env` re-reads `dist/.vite/manifest.json` per `vite_asset()` call in debug builds (so Vite watcher rebuilds are picked up immediately). Release builds load it once at startup. Gated on `cfg(debug_assertions)`.**Templates:** Jinja2 templates in `templates/`. `base.html` is the layout. Blog post content is rendered through a custom `BlogRenderer` (Mistune) that wraps blocks in `div.block-*` classes. A separate `PDFRenderer` exists for the PDF export route.**Request logging:** custom middleware in `src/main.rs::log_requests` prints `time METHOD STATUS latency path` per request, with ANSI-colored status codes (green 2xx, cyan 3xx, yellow 4xx, red 5xx). Always-on, costs sub-microsecond per request. The `.layer()` is applied after all routes so it covers `nest_service` static-file mounts and the `fallback` 404 handler.**Content:** `content/posts/` for markdown posts, `content/images/` for images served at `/content/images/`.## Layout```blog.bythewood.me/├── Cargo.toml, Cargo.lock # rust deps├── Makefile, README.md, bench/ # top-level├── src/ # rust source│ ├── main.rs # axum routes│ ├── posts.rs # frontmatter + post loading│ ├── markdown.rs # comrak custom renderer│ ├── templates.rs # minijinja env, url_for, vite_asset, Jinja2-compat formatter│ └── pdf.rs # chrome-headless-shell subprocess├── templates/ # jinja2 source (minijinja-compatible)├── content/ # markdown source (posts + images)├── frontend/ # JS pipeline (package.json, vite.config.js, static_src/, node_modules/)├── dist/ # vite build output (gitignored, served at /static/)├── target/ # cargo build output (gitignored)└── samplefiles/ # Caddyfile.sample, env.sample, post-receive.sample```The binary reads `templates/`, `dist/`, and `content/` from the current working directory by default. Override the project root with `BLOG_ROOT=<path>`.## Tooling- **Python deps:** managed with `uv` (see `pyproject.toml`, `uv.lock`)- **JS deps:** managed with `bun` (see `package.json`, `bun.lock`)- **Production:** Docker (Alpine-based) + Gunicorn, deployed via `docker-compose`- **Rust deps:** managed with `cargo` (see `Cargo.toml`, `Cargo.lock`)- **JS deps:** managed with `bun`, run from `frontend/` (see `frontend/package.json`, `frontend/bun.lock`)- **Production:** Docker (Alpine-based multi-stage, `rust:alpine` builder + `alpine:3.23` runtime), deployed via `docker-compose`. Runtime image installs `chromium` for PDF generation.## Key Routes- `/posts/<slug>/` — single post (old `/blog/<slug>/` 301-redirects here)- `/posts/<slug>/pdf/` — PDF export via WeasyPrint- `/posts/<slug>/pdf/` — PDF export via chrome-headless-shell- `/posts/<slug>/md/` — raw markdown download- `/blog/` — post index (also `/blog/tag/<tag>/` and `/blog/year/<year>/`)- `/search/live/` — JSON endpoint for live search- `/search/?q=...` — server-rendered search page- `/search/live/?q=...` — JSON endpoint for live search- `/og/<slug>.svg` — dynamic OG image generation
@@ -0,0 +1,1856 @@# This file is automatically @generated by Cargo.# It is not intended for manual editing.version = 4[[package]]name = "adler2"version = "2.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"[[package]]name = "aho-corasick"version = "1.1.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"dependencies = [ "memchr",][[package]]name = "android_system_properties"version = "0.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"dependencies = [ "libc",][[package]]name = "anstream"version = "1.0.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse",][[package]]name = "anstyle"version = "1.0.14"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"[[package]]name = "anstyle-parse"version = "1.0.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"dependencies = [ "utf8parse",][[package]]name = "anstyle-query"version = "1.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"dependencies = [ "windows-sys",][[package]]name = "anstyle-wincon"version = "3.0.11"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys",][[package]]name = "anyhow"version = "1.0.102"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"[[package]]name = "atomic-waker"version = "1.1.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"[[package]]name = "autocfg"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"[[package]]name = "axum"version = "0.8.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"dependencies = [ "axum-core", "bytes", "form_urlencoded", "futures-util", "http", "http-body", "http-body-util", "hyper", "hyper-util", "itoa", "matchit", "memchr", "mime", "percent-encoding", "pin-project-lite", "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", "sync_wrapper", "tokio", "tower", "tower-layer", "tower-service", "tracing",][[package]]name = "axum-core"version = "0.5.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"dependencies = [ "bytes", "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", "sync_wrapper", "tower-layer", "tower-service", "tracing",][[package]]name = "base64"version = "0.22.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"[[package]]name = "bincode"version = "1.3.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"dependencies = [ "serde",][[package]]name = "bit-set"version = "0.8.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"dependencies = [ "bit-vec",][[package]]name = "bit-vec"version = "0.8.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"[[package]]name = "bitflags"version = "2.11.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"[[package]]name = "blog"version = "0.1.0"dependencies = [ "anyhow", "axum", "chrono", "comrak", "mime_guess", "minijinja", "once_cell", "rand", "serde", "serde_json", "tempfile", "tokio", "tower", "tower-http", "urlencoding",][[package]]name = "bon"version = "3.9.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe"dependencies = [ "bon-macros", "rustversion",][[package]]name = "bon-macros"version = "3.9.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c"dependencies = [ "darling", "ident_case", "prettyplease", "proc-macro2", "quote", "rustversion", "syn",][[package]]name = "bumpalo"version = "3.20.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"[[package]]name = "bytes"version = "1.11.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"[[package]]name = "caseless"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8"dependencies = [ "unicode-normalization",][[package]]name = "cc"version = "1.2.61"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"dependencies = [ "find-msvc-tools", "shlex",][[package]]name = "cfg-if"version = "1.0.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"[[package]]name = "chrono"version = "0.4.44"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"dependencies = [ "iana-time-zone", "num-traits", "windows-link",][[package]]name = "clap"version = "4.6.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"dependencies = [ "clap_builder", "clap_derive",][[package]]name = "clap_builder"version = "4.6.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size",][[package]]name = "clap_derive"version = "4.6.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"dependencies = [ "heck", "proc-macro2", "quote", "syn",][[package]]name = "clap_lex"version = "1.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"[[package]]name = "colorchoice"version = "1.0.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"[[package]]name = "comrak"version = "0.30.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "949f8e6b02ebac005a8be2df9ec0876cafc83fdb9c510796c37f0fadf92dcd0e"dependencies = [ "bon", "caseless", "clap", "entities", "memchr", "once_cell", "regex", "shell-words", "slug", "syntect", "typed-arena", "unicode_categories", "xdg",][[package]]name = "core-foundation-sys"version = "0.8.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"[[package]]name = "crc32fast"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"dependencies = [ "cfg-if",][[package]]name = "darling"version = "0.23.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"dependencies = [ "darling_core", "darling_macro",][[package]]name = "darling_core"version = "0.23.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", "syn",][[package]]name = "darling_macro"version = "0.23.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"dependencies = [ "darling_core", "quote", "syn",][[package]]name = "deranged"version = "0.5.8"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"dependencies = [ "powerfmt",][[package]]name = "deunicode"version = "1.6.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04"[[package]]name = "entities"version = "1.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"[[package]]name = "equivalent"version = "1.0.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"[[package]]name = "errno"version = "0.3.14"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"dependencies = [ "libc", "windows-sys",][[package]]name = "fancy-regex"version = "0.16.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"dependencies = [ "bit-set", "regex-automata", "regex-syntax",][[package]]name = "fastrand"version = "2.4.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"[[package]]name = "find-msvc-tools"version = "0.1.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"[[package]]name = "flate2"version = "1.1.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"dependencies = [ "crc32fast", "miniz_oxide",][[package]]name = "fnv"version = "1.0.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"[[package]]name = "foldhash"version = "0.1.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"[[package]]name = "form_urlencoded"version = "1.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"dependencies = [ "percent-encoding",][[package]]name = "futures-channel"version = "0.3.32"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"dependencies = [ "futures-core",][[package]]name = "futures-core"version = "0.3.32"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"[[package]]name = "futures-sink"version = "0.3.32"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"[[package]]name = "futures-task"version = "0.3.32"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"[[package]]name = "futures-util"version = "0.3.32"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"dependencies = [ "futures-core", "futures-task", "pin-project-lite", "slab",][[package]]name = "getrandom"version = "0.2.17"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"dependencies = [ "cfg-if", "libc", "wasi",][[package]]name = "getrandom"version = "0.4.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", "wasip3",][[package]]name = "hashbrown"version = "0.15.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"dependencies = [ "foldhash",][[package]]name = "hashbrown"version = "0.17.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"[[package]]name = "heck"version = "0.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"[[package]]name = "http"version = "1.4.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"dependencies = [ "bytes", "itoa",][[package]]name = "http-body"version = "1.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"dependencies = [ "bytes", "http",][[package]]name = "http-body-util"version = "0.1.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"dependencies = [ "bytes", "futures-core", "http", "http-body", "pin-project-lite",][[package]]name = "http-range-header"version = "0.4.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"[[package]]name = "httparse"version = "1.10.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"[[package]]name = "httpdate"version = "1.0.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"[[package]]name = "hyper"version = "1.9.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"dependencies = [ "atomic-waker", "bytes", "futures-channel", "futures-core", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", "smallvec", "tokio",][[package]]name = "hyper-util"version = "0.1.20"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"dependencies = [ "bytes", "http", "http-body", "hyper", "pin-project-lite", "tokio", "tower-service",][[package]]name = "iana-time-zone"version = "0.1.65"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core",][[package]]name = "iana-time-zone-haiku"version = "0.1.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"dependencies = [ "cc",][[package]]name = "id-arena"version = "2.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"[[package]]name = "ident_case"version = "1.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"[[package]]name = "indexmap"version = "2.14.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"dependencies = [ "equivalent", "hashbrown 0.17.0", "serde", "serde_core",][[package]]name = "is_terminal_polyfill"version = "1.70.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"[[package]]name = "itoa"version = "1.0.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"[[package]]name = "js-sys"version = "0.3.97"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"dependencies = [ "cfg-if", "futures-util", "once_cell", "wasm-bindgen",][[package]]name = "leb128fmt"version = "0.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"[[package]]name = "libc"version = "0.2.186"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"[[package]]name = "linked-hash-map"version = "0.5.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"[[package]]name = "linux-raw-sys"version = "0.12.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"[[package]]name = "lock_api"version = "0.4.14"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"dependencies = [ "scopeguard",][[package]]name = "log"version = "0.4.29"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"[[package]]name = "matchit"version = "0.8.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"[[package]]name = "memchr"version = "2.8.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"[[package]]name = "memo-map"version = "0.3.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"[[package]]name = "mime"version = "0.3.17"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"[[package]]name = "mime_guess"version = "2.0.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"dependencies = [ "mime", "unicase",][[package]]name = "minijinja"version = "2.19.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "805bfd7352166bae857ee569628b52bcd85a1cecf7810861ebceb1686b72b75d"dependencies = [ "memo-map", "serde",][[package]]name = "miniz_oxide"version = "0.8.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"dependencies = [ "adler2", "simd-adler32",][[package]]name = "mio"version = "1.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"dependencies = [ "libc", "wasi", "windows-sys",][[package]]name = "num-conv"version = "0.2.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"[[package]]name = "num-traits"version = "0.2.19"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"dependencies = [ "autocfg",][[package]]name = "once_cell"version = "1.21.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"[[package]]name = "once_cell_polyfill"version = "1.70.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"[[package]]name = "onig"version = "6.5.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"dependencies = [ "bitflags", "libc", "once_cell", "onig_sys",][[package]]name = "onig_sys"version = "69.9.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7"dependencies = [ "cc", "pkg-config",][[package]]name = "parking_lot"version = "0.12.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"dependencies = [ "lock_api", "parking_lot_core",][[package]]name = "parking_lot_core"version = "0.9.12"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link",][[package]]name = "percent-encoding"version = "2.3.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"[[package]]name = "pin-project-lite"version = "0.2.17"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"[[package]]name = "pkg-config"version = "0.3.33"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"[[package]]name = "plist"version = "1.9.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1"dependencies = [ "base64", "indexmap", "quick-xml", "serde", "time",][[package]]name = "powerfmt"version = "0.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"[[package]]name = "ppv-lite86"version = "0.2.21"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"dependencies = [ "zerocopy",][[package]]name = "prettyplease"version = "0.2.37"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"dependencies = [ "proc-macro2", "syn",][[package]]name = "proc-macro2"version = "1.0.106"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"dependencies = [ "unicode-ident",][[package]]name = "quick-xml"version = "0.39.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b"dependencies = [ "memchr",][[package]]name = "quote"version = "1.0.45"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"dependencies = [ "proc-macro2",][[package]]name = "r-efi"version = "6.0.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"[[package]]name = "rand"version = "0.8.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"dependencies = [ "libc", "rand_chacha", "rand_core",][[package]]name = "rand_chacha"version = "0.3.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"dependencies = [ "ppv-lite86", "rand_core",][[package]]name = "rand_core"version = "0.6.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"dependencies = [ "getrandom 0.2.17",][[package]]name = "redox_syscall"version = "0.5.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"dependencies = [ "bitflags",][[package]]name = "regex"version = "1.12.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax",][[package]]name = "regex-automata"version = "0.4.14"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"dependencies = [ "aho-corasick", "memchr", "regex-syntax",][[package]]name = "regex-syntax"version = "0.8.10"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"[[package]]name = "rustix"version = "1.1.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys",][[package]]name = "rustversion"version = "1.0.22"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"[[package]]name = "ryu"version = "1.0.23"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"[[package]]name = "same-file"version = "1.0.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"dependencies = [ "winapi-util",][[package]]name = "scopeguard"version = "1.2.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"[[package]]name = "semver"version = "1.0.28"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"[[package]]name = "serde"version = "1.0.228"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"dependencies = [ "serde_core", "serde_derive",][[package]]name = "serde_core"version = "1.0.228"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"dependencies = [ "serde_derive",][[package]]name = "serde_derive"version = "1.0.228"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"dependencies = [ "proc-macro2", "quote", "syn",][[package]]name = "serde_json"version = "1.0.149"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij",][[package]]name = "serde_path_to_error"version = "0.1.20"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"dependencies = [ "itoa", "serde", "serde_core",][[package]]name = "serde_urlencoded"version = "0.7.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"dependencies = [ "form_urlencoded", "itoa", "ryu", "serde",][[package]]name = "shell-words"version = "1.1.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"[[package]]name = "shlex"version = "1.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"[[package]]name = "signal-hook-registry"version = "1.4.8"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"dependencies = [ "errno", "libc",][[package]]name = "simd-adler32"version = "0.3.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"[[package]]name = "slab"version = "0.4.12"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"[[package]]name = "slug"version = "0.1.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"dependencies = [ "deunicode", "wasm-bindgen",][[package]]name = "smallvec"version = "1.15.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"[[package]]name = "socket2"version = "0.6.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"dependencies = [ "libc", "windows-sys",][[package]]name = "strsim"version = "0.11.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"[[package]]name = "syn"version = "2.0.117"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"dependencies = [ "proc-macro2", "quote", "unicode-ident",][[package]]name = "sync_wrapper"version = "1.0.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"[[package]]name = "syntect"version = "5.3.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"dependencies = [ "bincode", "fancy-regex", "flate2", "fnv", "once_cell", "onig", "plist", "regex-syntax", "serde", "serde_derive", "serde_json", "thiserror", "walkdir", "yaml-rust",][[package]]name = "tempfile"version = "3.27.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"dependencies = [ "fastrand", "getrandom 0.4.2", "once_cell", "rustix", "windows-sys",][[package]]name = "terminal_size"version = "0.4.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"dependencies = [ "rustix", "windows-sys",][[package]]name = "thiserror"version = "2.0.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"dependencies = [ "thiserror-impl",][[package]]name = "thiserror-impl"version = "2.0.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"dependencies = [ "proc-macro2", "quote", "syn",][[package]]name = "time"version = "0.3.47"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", "serde_core", "time-core", "time-macros",][[package]]name = "time-core"version = "0.1.8"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"[[package]]name = "time-macros"version = "0.2.27"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"dependencies = [ "num-conv", "time-core",][[package]]name = "tinyvec"version = "1.11.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"dependencies = [ "tinyvec_macros",][[package]]name = "tinyvec_macros"version = "0.1.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"[[package]]name = "tokio"version = "1.52.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386"dependencies = [ "bytes", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "windows-sys",][[package]]name = "tokio-macros"version = "2.7.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"dependencies = [ "proc-macro2", "quote", "syn",][[package]]name = "tokio-util"version = "0.7.18"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio",][[package]]name = "tower"version = "0.5.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"dependencies = [ "futures-core", "futures-util", "pin-project-lite", "sync_wrapper", "tokio", "tower-layer", "tower-service", "tracing",][[package]]name = "tower-http"version = "0.6.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a28f0d049ccfaa566e14e9663d304d8577427b368cb4710a20528690287a738b"dependencies = [ "bitflags", "bytes", "futures-core", "futures-util", "http", "http-body", "http-body-util", "http-range-header", "httpdate", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "tokio", "tokio-util", "tower-layer", "tower-service",][[package]]name = "tower-layer"version = "0.3.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"[[package]]name = "tower-service"version = "0.3.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"[[package]]name = "tracing"version = "0.1.44"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"dependencies = [ "log", "pin-project-lite", "tracing-core",][[package]]name = "tracing-core"version = "0.1.36"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"dependencies = [ "once_cell",][[package]]name = "typed-arena"version = "2.0.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"[[package]]name = "unicase"version = "2.9.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"[[package]]name = "unicode-ident"version = "1.0.24"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"[[package]]name = "unicode-normalization"version = "0.1.25"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"dependencies = [ "tinyvec",][[package]]name = "unicode-xid"version = "0.2.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"[[package]]name = "unicode_categories"version = "0.1.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"[[package]]name = "urlencoding"version = "2.1.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"[[package]]name = "utf8parse"version = "0.2.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"[[package]]name = "walkdir"version = "2.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"dependencies = [ "same-file", "winapi-util",][[package]]name = "wasi"version = "0.11.1+wasi-snapshot-preview1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"[[package]]name = "wasip2"version = "1.0.3+wasi-0.2.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"dependencies = [ "wit-bindgen 0.57.1",][[package]]name = "wasip3"version = "0.4.0+wasi-0.3.0-rc-2026-01-06"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"dependencies = [ "wit-bindgen 0.51.0",][[package]]name = "wasm-bindgen"version = "0.2.120"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared",][[package]]name = "wasm-bindgen-macro"version = "0.2.120"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"dependencies = [ "quote", "wasm-bindgen-macro-support",][[package]]name = "wasm-bindgen-macro-support"version = "0.2.120"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared",][[package]]name = "wasm-bindgen-shared"version = "0.2.120"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"dependencies = [ "unicode-ident",][[package]]name = "wasm-encoder"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"dependencies = [ "leb128fmt", "wasmparser",][[package]]name = "wasm-metadata"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"dependencies = [ "anyhow", "indexmap", "wasm-encoder", "wasmparser",][[package]]name = "wasmparser"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"dependencies = [ "bitflags", "hashbrown 0.15.5", "indexmap", "semver",][[package]]name = "winapi-util"version = "0.1.11"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"dependencies = [ "windows-sys",][[package]]name = "windows-core"version = "0.62.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings",][[package]]name = "windows-implement"version = "0.60.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"dependencies = [ "proc-macro2", "quote", "syn",][[package]]name = "windows-interface"version = "0.59.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"dependencies = [ "proc-macro2", "quote", "syn",][[package]]name = "windows-link"version = "0.2.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"[[package]]name = "windows-result"version = "0.4.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"dependencies = [ "windows-link",][[package]]name = "windows-strings"version = "0.5.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"dependencies = [ "windows-link",][[package]]name = "windows-sys"version = "0.61.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"dependencies = [ "windows-link",][[package]]name = "wit-bindgen"version = "0.51.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"dependencies = [ "wit-bindgen-rust-macro",][[package]]name = "wit-bindgen"version = "0.57.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"[[package]]name = "wit-bindgen-core"version = "0.51.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"dependencies = [ "anyhow", "heck", "wit-parser",][[package]]name = "wit-bindgen-rust"version = "0.51.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"dependencies = [ "anyhow", "heck", "indexmap", "prettyplease", "syn", "wasm-metadata", "wit-bindgen-core", "wit-component",][[package]]name = "wit-bindgen-rust-macro"version = "0.51.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"dependencies = [ "anyhow", "prettyplease", "proc-macro2", "quote", "syn", "wit-bindgen-core", "wit-bindgen-rust",][[package]]name = "wit-component"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"dependencies = [ "anyhow", "bitflags", "indexmap", "log", "serde", "serde_derive", "serde_json", "wasm-encoder", "wasm-metadata", "wasmparser", "wit-parser",][[package]]name = "wit-parser"version = "0.244.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"dependencies = [ "anyhow", "id-arena", "indexmap", "log", "semver", "serde", "serde_derive", "serde_json", "unicode-xid", "wasmparser",][[package]]name = "xdg"version = "2.5.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"[[package]]name = "yaml-rust"version = "0.4.5"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"dependencies = [ "linked-hash-map",][[package]]name = "zerocopy"version = "0.8.48"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"dependencies = [ "zerocopy-derive",][[package]]name = "zerocopy-derive"version = "0.8.48"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"dependencies = [ "proc-macro2", "quote", "syn",][[package]]name = "zmij"version = "1.0.21"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
@@ -0,0 +1,26 @@[package]name = "blog"version = "0.1.0"edition = "2021"[dependencies]axum = "0.8"tokio = { version = "1", features = ["full"] }tower = { version = "0.5", features = ["util"] }tower-http = { version = "0.6", features = ["fs", "set-header"] }minijinja = { version = "2", features = ["loader", "loop_controls"] }comrak = "0.30"serde = { version = "1", features = ["derive"] }serde_json = "1"chrono = { version = "0.4", default-features = false, features = ["clock"] }rand = "0.8"urlencoding = "2"once_cell = "1"tempfile = "3"anyhow = "1"mime_guess = "2"[profile.release]lto = truecodegen-units = 1strip = true
@@ -1,24 +1,38 @@FROM python:3.13-alpine# ----- builder -----FROM rust:alpine AS builderCOPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/COPY --from=oven/bun:alpine /usr/local/bin/bun /usr/local/bin/bunRUN apk add --no-cache musl-devRUN apk add --update --no-cache \ libstdc++ libgcc \ pango fontconfig font-noto font-jetbrains-monoCOPY --from=oven/bun:alpine /usr/local/bin/bun /usr/local/bin/bunWORKDIR /appCOPY pyproject.toml uv.lock package.json bun.lock ./RUN bun install --frozen-lockfile && uv sync --frozen --no-devCOPY Cargo.toml Cargo.lock ./COPY src ./srcCOPY frontend ./frontendCOPY . .RUN cd frontend && bun install --frozen-lockfile && bun run buildRUN cargo build --releaseENV PATH="/app/.venv/bin:/app/node_modules/.bin:$PATH"# ----- runtime -----FROM alpine:3.23RUN bun run buildRUN apk add --no-cache \ chromium font-jetbrains-mono ttf-dejavuWORKDIR /appCOPY --from=builder /app/target/release/blog ./blogCOPY --from=builder /app/dist ./distCOPY templates ./templatesCOPY content ./contentRUN addgroup -S -g 1000 app && \ adduser -S -h /app -s /sbin/nologin -u 1000 -G app app && \ chown -R app:app /appUSER appENV PORT=8000EXPOSE 8000CMD ["./blog"]
@@ -1,10 +1,36 @@.PHONY: run push buildCARGO ?= $(HOME)/.cargo/bin/cargoPORT ?= 8000run: bun run dev & uv run flask --app app run --host 0.0.0.0 --port 8000 --debug.DEFAULT_GOAL := run.PHONY: run build start bench clean pushbuild: bun run build# Dev: Vite watch + cargo run concurrently. Both die on Ctrl+C.run: frontend/node_modules dist/.vite/manifest.json @trap 'kill 0' EXIT INT TERM; \ (cd frontend && bun run dev) & \ PORT=$(PORT) $(CARGO) run# Production build (Vite assets + release binary)build: frontend/node_modules cd frontend && bun run build $(CARGO) build --release# Run the release binary (after `make build`)start: PORT=$(PORT) ./target/release/blogbench: bench/run.shclean: $(CARGO) clean rm -rf dist frontend/node_modulespush: git remote | xargs -I R git push R masterfrontend/node_modules: cd frontend && bun installdist/.vite/manifest.json: frontend/node_modules cd frontend && bun run build
@@ -1,138 +1,76 @@# BlogA self-hostable blog built on Flask for developers.## MotivationI was bored and felt like writing my own blog over the weekend.## Features- Top notch SEO using industry best practices and multiple scanners to detect issues on a regular basis.- Built on Flask with markdown files for content, no database required.- Customized for developers, makes use of CodeMirror for syntax highlighting.- Easily adjusted to fit your needs with Bootstrap for the design and easy to adjust hosting options using Docker.- Minimal network payloads, out of the box 100% scores on all lighthouse metrics and most pages, even with a few images, are less than 300kB in size.## RequirementsYou need docker + docker-compose installed for a quick production start or youcan figure out how we install and run things via the `Dockerfile` and set it upyourself.If you want to install things without docker then you'll need the followingdependencies:- python- uv- bunYou can also check the `Dockerfile` for an exact list of dependencies and adjustpackage names for your desired platform.## Running locallyIf you have all of the above dependencies installed you can use my Makefile torun and install python and node dependencies locally. Running `make run` willstart both the Vite watcher and the Flask dev server.## Checking outdated dependenciesThis can be done in both bun and uv with the following two commands: uv lock --upgrade --dry-run bun outdatedYou can then upgrade the outdated dependencies with the following two commands: uv lock --upgrade bun updateI recommend testing everything after this to make sure it's all working.## Optimizing images with webpMy development system runs Ubuntu so I installed the official webp utils fromGoogle with `apt install webp`. cwebp -q 90 -m 6 -o output.webp input.png## Using docker-composeThe easiest way to run this project is to run it using`docker-compose up --build -d` if you have `docker-compose` and `docker`installed. This will start the server and have you running at port 8000. Makesure you setup the `.env` file before running, you can copy the sample from`samplefiles/env.sample` into the root of the project as `.env` and change thevariables.## BackupsAll data is stored in the repo itself (markdown files in `content/posts/` andimages in `content/images/`). Back up the repo and you have everything.## SupportI won't be providing any user support for this project. I'm more than happy toaccept good pull requests and fix bugs but I don't have the time to help peoplerun or use this project. I appologize in advance for this. Maintainingmutliple OSS projects has taught me that I need to step back from trying toprovide support to avoid burnout.## Server guideThis quickstart requires that you have an Alpine Linux server running with adomain name pointed to it. I'm currently using Linode as my host since theysupport Alpine Linux nicely. If you don't want to use Linode or Alpine Linuxyou can use these instructions and just change the apk commands at the start towhatever Linux distro you're using.**IMPORTANT NOTE**: Change `blog.bythewood.me` to your domain name whererelevant in these instructions.**TIP**: During the ufw portion to enable the firewall I recommend only allowingyour IP address or your ISP's IP address range which you can find on whoislookups at the top. For example, replace `192.230.176.0/20` with your IP or yourISP's IP range. ufw allow from 192.230.176.0/20 proto tcp to any port 22I allow my local ISP's range because I have a DHCP lease from them and I gettired of logging into my server from my hosting provider's UI to update it. It'sgood enough security and much better than nothing!Server: apk update && apk upgrade && apk add docker docker-compose caddy git iptables ip6tables ufw ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw --force enable echo -e "#!/bin/sh\napk upgrade --update | sed \"s/^/[\`date\`] /\" >> /var/log/apk-autoupgrade.log" > /etc/periodic/daily/apk-autoupgrade && chmod 700 /etc/periodic/daily/apk-autoupgrade rc-update add docker boot && service docker start mkdir -p /srv/git/blog.bythewood.me.git && cd /srv/git/blog.bythewood.me.git && git init --bareLocal: git clone git@github.com:overshard/blog.bythewood.me.git && cd blog.bythewood.me git remote remove origin && git remote add origin root@blog.bythewood.me:/srv/git/blog.bythewood.me.git git push --set-upstream origin masterServer: mkdir -p /srv/docker && cd /srv/docker && git clone /srv/git/blog.bythewood.me.git blog.bythewood.me && cd /srv/docker/blog.bythewood.me cp samplefiles/Caddyfile.sample /etc/caddy/Caddyfile && sed -i 's/blog.example.com/blog.bythewood.me/g' /etc/caddy/Caddyfile cp samplefiles/env.sample .env && sed -i 's/blog.example.com/blog.bythewood.me/g' .env cp samplefiles/post-receive.sample /srv/git/blog.bythewood.me.git/hooks/post-receive docker-compose up --build --detach rc-update add caddy boot && service caddy start# blog.bythewood.mePersonal blog, served by a single Rust axum binary. Self-contained: posts,templates, Vite-built static assets, and the binary all live here.## Stack| Concern | Crate / Tool ||-----------------|----------------------------|| Web framework | axum + tokio || Template engine | minijinja || Markdown | comrak || PDF | headless Chromium || Static assets | Vite + Bun |Crate selection rationale (short version): axum is the most-pulled asyncframework, minijinja is the only Rust engine that accepts upstream Jinja2syntax, comrak's `partial-formatter` story is the closest match toMistune's renderer-override pattern.## Layout```blog.bythewood.me/├── Cargo.toml, Cargo.lock # rust deps├── Makefile, README.md, bench/ # top-level├── src/ # rust source│ ├── main.rs # axum routes│ ├── posts.rs # frontmatter + post loading│ ├── markdown.rs # comrak custom renderer│ ├── templates.rs # minijinja env, url_for, vite_asset, Jinja2-compat formatter│ └── pdf.rs # chrome-headless-shell subprocess├── templates/ # jinja2 source (minijinja-compatible)├── content/ # markdown source│ ├── posts/ # markdown posts with YAML frontmatter│ └── images/ # served at /content/images/├── frontend/ # JS pipeline│ ├── package.json, bun.lock, vite.config.js│ ├── static_src/ # SCSS, JS, font imports│ └── node_modules/ # gitignored├── dist/ # vite build output (gitignored, rebuilt by Docker, served at /static/)└── target/ # cargo build output (gitignored)```The binary reads `templates/`, `dist/`, and `content/` from the currentworking directory. Override with `BLOG_ROOT=<path>`.## RunningDev (Vite watch + cargo run, port 8000):```shmake run```Production build (Vite assets + release binary):```shmake buildmake start```Override port: `PORT=8001 make start`.## Bench`bench/run.sh` runs an `oha` load test sweep across the main routes. Bydefault it compares against a Flask server on port 8002 (the original`blog.bythewood.me`).## Caveats- **PDF cold start.** Every PDF request spawns a fresh chromium process (~500 ms first hit). A persistent chromium with a remote-debugging port would close this gap but adds significant code.- **Posts loaded once at startup.** Add a post, restart the process.
@@ -1,539 +0,0 @@"""app.pyA simple Flask blog powered by markdown files."""import htmlimport jsonimport mathimport osimport randomfrom datetime import date, datetimeimport mistunefrom flask import ( Flask, Response, abort, jsonify, redirect, render_template, request, send_from_directory, url_for,)from urllib.parse import unquote, urlparsefrom weasyprint import HTML, default_url_fetcherapp = Flask(__name__)app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 31536000CONTENT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "content")MANIFEST_PATH = os.path.join(app.static_folder, ".vite", "manifest.json")_manifest_cache = Nonedef get_vite_manifest(): global _manifest_cache if _manifest_cache is None or app.debug: with open(MANIFEST_PATH) as f: _manifest_cache = json.load(f) return _manifest_cachedef vite_asset(entry, kind="file"): manifest = get_vite_manifest() chunk = manifest.get(entry) if chunk: if kind == "css" and "css" in chunk: return "/static/" + chunk["css"][0] return "/static/" + chunk["file"] return url_for("static", filename=entry)app.jinja_env.globals["vite_asset"] = vite_asset# -- Markdown rendering -------------------------------------------------------class BlogRenderer(mistune.HTMLRenderer): def block_code(self, code, info=None, **attrs): lang = info or "" if lang == "html": lang = "htmlmixed" escaped = html.escape(code) return ( f'<div class="block-code">' f'<textarea data-language="{lang}">{escaped}</textarea>' f"</div>\n" ) def image(self, text, url, title=None): if url.startswith("images/"): url = url[len("images/"):] return ( f'<div class="block-image">' f'<img src="/content/images/{url}" class="rounded" alt="{text}">' f"</div>\n" ) def paragraph(self, text): if text.strip().startswith('<div class="block-image">'): return text return f'<div class="block-rich-text"><p>{text}</p></div>\n' def list(self, text, ordered, **attrs): tag = "ol" if ordered else "ul" return f'<div class="block-rich-text"><{tag}>{text}</{tag}></div>\n' def heading(self, text, level, **attrs): return f'<div class="block-rich-text"><h{level}>{text}</h{level}></div>\n' def block_quote(self, text): return f'<div class="block-rich-text"><blockquote>{text}</blockquote></div>\n' def thematic_break(self): return '<div class="block-rich-text"><hr></div>\n'markdown = mistune.create_markdown(renderer=BlogRenderer(), plugins=["strikethrough"])class PDFRenderer(BlogRenderer): def block_code(self, code, info=None, **attrs): lang = info or "" escaped = html.escape(code) return ( f'<div class="block-code">' f'<pre><code class="language-{lang}">{escaped}</code></pre>' f"</div>\n" )markdown_pdf = mistune.create_markdown(renderer=PDFRenderer(), plugins=["strikethrough"])# -- Content loading -----------------------------------------------------------def parse_frontmatter(text): """Parse YAML-like frontmatter from a markdown file.""" if not text.startswith("---"): return {}, text end = text.find("---", 3) if end == -1: return {}, text meta = {} for line in text[3:end].strip().split("\n"): if ": " in line: key, value = line.split(": ", 1) meta[key.strip()] = value.strip() body = text[end + 3 :].strip() return meta, bodydef load_posts(): """Load all markdown posts from content/posts/.""" posts = [] posts_dir = os.path.join(CONTENT_DIR, "posts") if not os.path.exists(posts_dir): return posts for filename in os.listdir(posts_dir): if not filename.endswith(".md"): continue filepath = os.path.join(posts_dir, filename) with open(filepath) as f: text = f.read() meta, body = parse_frontmatter(text) tags = [t.strip() for t in meta.get("tags", "").split(",") if t.strip()] post_date = meta.get("date", "") publish_date = meta.get("publish_date", post_date) post = { "filename": filename, "title": meta.get("title", ""), "slug": meta.get("slug", filename[:-3]), "date": post_date, "publish_date": publish_date, "tags": tags, "description": meta.get("description", ""), "cover_image": meta.get("cover_image", ""), "body_html": markdown(body), "body_html_pdf": markdown_pdf(body), "read_time": max(1, math.ceil(len(body.split()) / 200)), } posts.append(post) posts.sort(key=lambda p: p["date"], reverse=True) return postsPOSTS = load_posts()POSTS_BY_SLUG = {p["slug"]: p for p in POSTS}@app.before_requestdef reload_posts_in_debug(): global POSTS, POSTS_BY_SLUG if app.debug: POSTS = load_posts() POSTS_BY_SLUG = {p["slug"]: p for p in POSTS}# -- Helpers -------------------------------------------------------------------def get_published_posts(): """Return posts where publish_date <= today.""" today = date.today().isoformat() return [p for p in POSTS if p["publish_date"] <= today]def get_tags(posts): """Return sorted list of unique tags with counts and URLs.""" counts = {} for post in posts: for tag in post["tags"]: counts[tag] = counts.get(tag, 0) + 1 return sorted( [{"name": t, "slug": t, "count": c, "url": url_for("blog_tag", tag=t)} for t, c in counts.items()], key=lambda t: t["name"], )def get_years(posts): """Return sorted list of unique years.""" return sorted(set(p["date"][:4] for p in posts if p["date"]), reverse=True)def get_related(post, posts, count=3): """Find posts with the most overlapping tags.""" if not post["tags"]: return posts[:count] scored = [] for p in posts: if p["slug"] == post["slug"]: continue overlap = len(set(post["tags"]) & set(p["tags"])) if overlap > 0: scored.append((overlap, p)) scored.sort(key=lambda x: x[0], reverse=True) related = [p for _, p in scored[:count]] if len(related) < count: related_slugs = {p["slug"] for p in related} for p in posts: if p["slug"] != post["slug"] and p["slug"] not in related_slugs: related.append(p) if len(related) >= count: break return related# -- Context processor ---------------------------------------------------------@app.errorhandler(404)def page_not_found(e): return render_template("404.html", page={"title": "404", "description": "Page not found"}), 404@app.context_processordef inject_globals(): posts = get_published_posts() return { "nav_items": get_tags(posts), "now": datetime.now(), "debug": app.debug, }# -- Routes --------------------------------------------------------------------@app.route("/")def index(): posts = get_published_posts() latest_post = posts[0] if posts else None rest = [p for p in posts if p != latest_post] random_posts = random.sample(rest, min(3, len(rest))) return render_template( "home.html", page={"title": "Isaac Bythewood's Blog", "slug": "home", "description": "Writing about webdev, infrastructure, security, and tooling by Isaac Bythewood, a Senior Solutions Architect in Elkin, NC."}, latest_post=latest_post, random_blog_posts=random_posts, )@app.route("/blog/")def blog_index(): posts = get_published_posts() return render_template( "blog_index.html", page={"title": "Blog", "slug": "blog", "description": "Posts on webdev, coding, security, and sysadmin by Isaac Bythewood."}, blog_posts=posts, tags=get_tags(posts), years=get_years(posts), breadcrumbs=[{"title": "Home", "url": url_for("index")}], )@app.route("/posts/<slug>/")def blog_post(slug): post = POSTS_BY_SLUG.get(slug) if not post or post["publish_date"] > date.today().isoformat(): abort(404) posts = get_published_posts() return render_template( "blog_post.html", page=post, post=post, related_posts=get_related(post, posts), breadcrumbs=[ {"title": "Home", "url": url_for("index")}, {"title": "Blog", "url": url_for("blog_index")}, ], )def pdf_url_fetcher(url): path = unquote(urlparse(url).path) local_roots = { "/content/images/": os.path.join(CONTENT_DIR, "images"), "/static/": app.static_folder, } for prefix, root in local_roots.items(): if path.startswith(prefix): file_path = os.path.join(root, path[len(prefix):]) if os.path.isfile(file_path): return {"file_obj": open(file_path, "rb")} return default_url_fetcher(url)@app.route("/posts/<slug>/pdf/")def blog_post_pdf(slug): post = POSTS_BY_SLUG.get(slug) if not post or post["publish_date"] > date.today().isoformat(): abort(404) html_content = render_template("blog_post_pdf.html", post=post) pdf = HTML( string=html_content, base_url=request.url_root, url_fetcher=pdf_url_fetcher, ).write_pdf() return Response( pdf, mimetype="application/pdf", headers={"Content-Disposition": f'filename="{post["slug"]}.pdf"'}, )@app.route("/posts/<slug>/md/")def blog_post_md(slug): post = POSTS_BY_SLUG.get(slug) if not post or post["publish_date"] > date.today().isoformat(): abort(404) filepath = os.path.join(CONTENT_DIR, "posts", post["filename"]) with open(filepath) as f: content = f.read() return Response( content, mimetype="text/markdown", headers={"Content-Disposition": f'filename="{post["slug"]}.md"'}, )@app.route("/blog/<slug>/")def blog_post_redirect(slug): return redirect(url_for("blog_post", slug=slug), code=301)@app.route("/blog/<slug>/pdf/")def blog_post_pdf_redirect(slug): return redirect(url_for("blog_post_pdf", slug=slug), code=301)@app.route("/blog/<slug>/md/")def blog_post_md_redirect(slug): return redirect(url_for("blog_post_md", slug=slug), code=301)@app.route("/blog/tag/<tag>/")def blog_tag(tag): posts = get_published_posts() filtered = [p for p in posts if tag in p["tags"]] if not filtered: abort(404) extra_posts = None if len(filtered) < 5: extra_posts = [p for p in posts if tag not in p["tags"]][:4] return render_template( "blog_index.html", page={"title": f"Tag: {tag}", "slug": f"tag-{tag}", "description": f"Posts tagged {tag}"}, blog_posts=filtered, extra_posts=extra_posts, active_tag={"name": tag, "slug": tag}, tags=get_tags(posts), years=get_years(posts), breadcrumbs=[ {"title": "Home", "url": url_for("index")}, {"title": "Blog", "url": url_for("blog_index")}, ], )@app.route("/blog/year/<int:year>/")def blog_year(year): posts = get_published_posts() filtered = [p for p in posts if p["date"].startswith(str(year))] if not filtered: abort(404) extra_posts = None if len(filtered) < 5: extra_posts = [p for p in posts if not p["date"].startswith(str(year))][:4] return render_template( "blog_index.html", page={"title": f"Year: {year}", "slug": f"year-{year}", "description": f"Posts from {year}"}, blog_posts=filtered, extra_posts=extra_posts, active_year=str(year), tags=get_tags(posts), years=get_years(posts), breadcrumbs=[ {"title": "Home", "url": url_for("index")}, {"title": "Blog", "url": url_for("blog_index")}, ], )@app.route("/search/")def search(): q = request.args.get("q", "") posts = get_published_posts() results = [] random_posts = None if q: ql = q.lower() for p in posts: if ( ql in p["title"].lower() or ql in p["description"].lower() or any(ql in t.lower() for t in p["tags"]) ): results.append(p) else: random_posts = random.sample(posts, min(6, len(posts))) return render_template( "search.html", page={"title": "Search", "slug": "search", "description": "Search posts on webdev, coding, security, and sysadmin."}, results=results, random_posts=random_posts, q=q, breadcrumbs=[{"title": "Home", "url": url_for("index")}], )@app.route("/search/live/")def search_live(): q = request.args.get("q", "") posts = get_published_posts() results = [] if q: ql = q.lower() for p in posts: if ( ql in p["title"].lower() or ql in p["description"].lower() or any(ql in t.lower() for t in p["tags"]) ): results.append( { "title": p["title"], "description": p["description"], "url": url_for("blog_post", slug=p["slug"]), } ) if len(results) >= 5: break return jsonify(results)@app.route("/content/images/<path:filename>")def content_images(filename): return send_from_directory(os.path.join(CONTENT_DIR, "images"), filename)@app.route("/og/<slug>.svg")def og_image(slug): post = POSTS_BY_SLUG.get(slug) if post: title = post["title"] tags = post["tags"] else: title = "Isaac Bythewood's Blog" tags = [] # Wrap title into lines (~35 chars each) words = title.split() lines = [] current = "" for word in words: if current and len(current) + len(word) + 1 > 35: lines.append(current) current = word else: current = f"{current} {word}" if current else word if current: lines.append(current) return ( render_template( "og.svg", title_lines=lines[:3], tags=tags, ), 200, {"Content-Type": "image/svg+xml"}, )@app.route("/favicon.ico")def favicon(): return render_template("favicon.svg"), 200, {"Content-Type": "image/svg+xml"}@app.route("/robots.txt")def robots(): return render_template("robots.txt"), 200, {"Content-Type": "text/plain"}@app.route("/sitemap.xml")def sitemap(): posts = get_published_posts() tags = get_tags(posts) years = get_years(posts) # Compute lastmod for tag and year pages tag_lastmod = {} year_lastmod = {} for p in posts: for t in p["tags"]: if t not in tag_lastmod or p["date"] > tag_lastmod[t]: tag_lastmod[t] = p["date"] y = p["date"][:4] if y not in year_lastmod or p["date"] > year_lastmod[y]: year_lastmod[y] = p["date"] return ( render_template( "sitemap.xml", posts=posts, tags=tags, years=years, tag_lastmod=tag_lastmod, year_lastmod=year_lastmod, ), 200, {"Content-Type": "application/xml"}, )
modified
docker-compose.yml
@@ -5,5 +5,6 @@ services: env_file: .env ports: - "127.0.0.1:${PORT}:${PORT}" command: uv run gunicorn -b 0.0.0.0:${PORT} -w 2 app:app environment: PORT: ${PORT} restart: unless-stopped
renamed
bun.lock → frontend/bun.lock
renamed
package.json → frontend/package.json
renamed
static_src/index.js → frontend/static_src/index.js
renamed
static_src/scripts/bootstrap.js → frontend/static_src/scripts/bootstrap.js
renamed
static_src/scripts/code.js → frontend/static_src/scripts/code.js
renamed
static_src/scripts/dark.js → frontend/static_src/scripts/dark.js
renamed
static_src/scripts/search.js → frontend/static_src/scripts/search.js
renamed
static_src/styles/_variables.scss → frontend/static_src/styles/_variables.scss
renamed
static_src/styles/base.scss → frontend/static_src/styles/base.scss
renamed
static_src/styles/blocks.scss → frontend/static_src/styles/blocks.scss
renamed
static_src/styles/bootstrap.scss → frontend/static_src/styles/bootstrap.scss
renamed
static_src/styles/code.scss → frontend/static_src/styles/code.scss
renamed
static_src/styles/dark.scss → frontend/static_src/styles/dark.scss
renamed
static_src/styles/home.scss → frontend/static_src/styles/home.scss
renamed
vite.config.js → frontend/vite.config.js
@@ -5,7 +5,7 @@ export default defineConfig({ root: "static_src", base: "/static/", build: { outDir: resolve(__dirname, "static"), outDir: resolve(__dirname, "../dist"), emptyOutDir: true, manifest: true, rollupOptions: {
@@ -1,10 +0,0 @@[project]name = "blog.bythewood.me"version = "0.1.0"requires-python = ">=3.13"dependencies = [ "flask>=3.1", "gunicorn>=23.0", "mistune>=3.1", "weasyprint>=68.1",]
@@ -0,0 +1,689 @@mod markdown;mod pdf;mod posts;mod templates;use axum::{ body::Body, extract::{Path as AxumPath, Query, Request, State}, http::{header, HeaderMap, StatusCode, Uri}, middleware::{self, Next}, response::{Html, IntoResponse, Redirect, Response}, routing::get, Router,};use chrono::{Datelike, Local};use minijinja::{context, Environment, Value};use rand::seq::SliceRandom;use serde::Deserialize;use std::collections::{BTreeMap, HashMap};use std::net::SocketAddr;use std::path::PathBuf;use std::sync::Arc;use std::time::Instant;use tower_http::services::ServeDir;use tower_http::set_header::SetResponseHeaderLayer;use posts::Post;use templates::RequestCtx;#[derive(Clone)]struct AppState { env: Arc<Environment<'static>>, posts: Arc<Vec<Post>>, posts_by_slug: Arc<HashMap<String, usize>>, content_dir: PathBuf, server_base: String,}#[tokio::main]async fn main() { let project_root: PathBuf = std::env::var("BLOG_ROOT") .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(".")); let templates_dir = project_root.join("templates"); let dist_dir = project_root.join("dist"); let content_dir = project_root.join("content"); let manifest_path = dist_dir.join(".vite/manifest.json"); let env = templates::build_env(&templates_dir, &manifest_path); let posts = posts::load_posts(&content_dir); let posts_by_slug: HashMap<String, usize> = posts .iter() .enumerate() .map(|(i, p)| (p.slug.clone(), i)) .collect(); let port: u16 = std::env::var("PORT") .ok() .and_then(|v| v.parse().ok()) .unwrap_or(8000); let state = AppState { env: Arc::new(env), posts: Arc::new(posts), posts_by_slug: Arc::new(posts_by_slug), content_dir: content_dir.clone(), server_base: format!("http://127.0.0.1:{port}"), }; let app = Router::new() .route("/", get(index)) .route("/blog/", get(blog_index)) .route("/posts/{slug}/", get(blog_post)) .route("/posts/{slug}/pdf/", get(blog_post_pdf)) .route("/posts/{slug}/md/", get(blog_post_md)) .route("/blog/{slug}/", get(blog_post_redirect)) .route("/blog/{slug}/pdf/", get(blog_post_pdf_redirect)) .route("/blog/{slug}/md/", get(blog_post_md_redirect)) .route("/blog/tag/{tag}/", get(blog_tag)) .route("/blog/year/{year}/", get(blog_year)) .route("/search/", get(search_page)) .route("/search/live/", get(search_live)) .route("/og/{slug_svg}", get(og_image)) .route("/favicon.ico", get(favicon)) .route("/robots.txt", get(robots)) .route("/sitemap.xml", get(sitemap)) .nest_service( "/static", tower::ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( header::CACHE_CONTROL, header::HeaderValue::from_static("public, max-age=31536000"), )) .service(ServeDir::new(&dist_dir)), ) .nest_service( "/content/images", tower::ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( header::CACHE_CONTROL, header::HeaderValue::from_static("public, max-age=31536000"), )) .service(ServeDir::new(content_dir.join("images"))), ) .fallback(not_found) .layer(middleware::from_fn(log_requests)) .with_state(state); let addr = SocketAddr::from(([0, 0, 0, 0], port)); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); eprintln!("blog listening on http://{addr}"); axum::serve(listener, app).await.unwrap();}fn today() -> String { Local::now().date_naive().format("%Y-%m-%d").to_string()}async fn log_requests(req: Request, next: Next) -> Response { let method = req.method().clone(); let path = req .uri() .path_and_query() .map(|p| p.as_str().to_string()) .unwrap_or_else(|| req.uri().path().to_string()); let start = Instant::now(); let response = next.run(req).await; let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0; let status = response.status().as_u16(); let now = Local::now().format("%H:%M:%S"); let color = match status { 200..=299 => "\x1b[32m", // green 300..=399 => "\x1b[36m", // cyan 400..=499 => "\x1b[33m", // yellow _ => "\x1b[31m", // red }; eprintln!( "{now} {method:<5} {color}{status}\x1b[0m {elapsed_ms:>7.2}ms {path}" ); response}fn published_posts(state: &AppState) -> Vec<Post> { let today = today(); state .posts .iter() .filter(|p| p.publish_date.as_str() <= today.as_str()) .cloned() .collect()}fn collect_tags(posts: &[Post]) -> Vec<TagEntry> { let mut counts: BTreeMap<String, usize> = BTreeMap::new(); for p in posts { for t in &p.tags { *counts.entry(t.clone()).or_insert(0) += 1; } } let mut out: Vec<TagEntry> = counts .into_iter() .map(|(name, count)| TagEntry { url: format!("/blog/tag/{}/", urlencoding::encode(&name)), slug: name.clone(), name, count, }) .collect(); out.sort_by(|a, b| a.name.cmp(&b.name)); out}fn collect_years(posts: &[Post]) -> Vec<String> { let mut years: Vec<String> = posts .iter() .filter(|p| !p.date.is_empty()) .map(|p| p.date[..4.min(p.date.len())].to_string()) .collect(); years.sort(); years.dedup(); years.reverse(); years}fn related(post: &Post, posts: &[Post], count: usize) -> Vec<Post> { if post.tags.is_empty() { return posts.iter().take(count).cloned().collect(); } let post_tags: std::collections::HashSet<&String> = post.tags.iter().collect(); let mut scored: Vec<(usize, &Post)> = posts .iter() .filter(|p| p.slug != post.slug) .map(|p| { let overlap = p.tags.iter().filter(|t| post_tags.contains(t)).count(); (overlap, p) }) .filter(|(o, _)| *o > 0) .collect(); scored.sort_by(|a, b| b.0.cmp(&a.0)); let mut out: Vec<Post> = scored .into_iter() .take(count) .map(|(_, p)| p.clone()) .collect(); if out.len() < count { let have: std::collections::HashSet<String> = out.iter().map(|p| p.slug.clone()).collect(); for p in posts { if p.slug == post.slug || have.contains(&p.slug) { continue; } out.push(p.clone()); if out.len() >= count { break; } } } out}#[derive(Debug, Clone, serde::Serialize)]struct TagEntry { name: String, slug: String, count: usize, url: String,}#[derive(Debug, Clone, serde::Serialize)]struct Crumb { title: String, url: String,}fn build_request(uri: &Uri, headers: &HeaderMap) -> RequestCtx { let host = headers .get(header::HOST) .and_then(|v| v.to_str().ok()) .unwrap_or("localhost"); let scheme = headers .get("x-forwarded-proto") .and_then(|v| v.to_str().ok()) .unwrap_or("http"); let url_root = format!("{scheme}://{host}/"); let path_and_query = uri.path_and_query().map(|p| p.as_str()).unwrap_or("/"); let url = format!("{scheme}://{host}{path_and_query}"); let base_url = format!("{scheme}://{host}{}", uri.path()); RequestCtx { url, url_root, base_url, }}fn render_html( state: &AppState, template: &str, extra: minijinja::Value, request: &RequestCtx,) -> Result<Html<String>, AppError> { let posts = published_posts(state); let nav_items = collect_tags(&posts); let now = NowCtx { year: Local::now().year() }; let tmpl = state.env.get_template(template)?; let ctx = context! { nav_items => nav_items, now => now, debug => false, request => request, ..extra }; let body = tmpl.render(ctx)?; Ok(Html(body))}#[derive(Debug, serde::Serialize)]struct NowCtx { year: i32,}struct AppError(StatusCode, String);impl AppError { fn not_found() -> Self { AppError(StatusCode::NOT_FOUND, "not found".to_string()) }}impl<E: std::fmt::Display> From<E> for AppError { fn from(e: E) -> Self { AppError(StatusCode::INTERNAL_SERVER_ERROR, format!("internal error: {e}")) }}impl IntoResponse for AppError { fn into_response(self) -> Response { (self.0, self.1).into_response() }}async fn index( State(state): State<AppState>, uri: Uri, headers: HeaderMap,) -> Result<Html<String>, AppError> { let request = build_request(&uri, &headers); let posts = published_posts(&state); let latest_post = posts.first().cloned(); let rest: Vec<Post> = match &latest_post { Some(latest) => posts.iter().filter(|p| p.slug != latest.slug).cloned().collect(), None => Vec::new(), }; let mut rng = rand::thread_rng(); let mut shuffled = rest.clone(); shuffled.shuffle(&mut rng); let random_blog_posts: Vec<Post> = shuffled.into_iter().take(3).collect(); let page = context! { title => "Isaac Bythewood's Blog", slug => "home", description => "Writing about webdev, infrastructure, security, and tooling by Isaac Bythewood, a Senior Solutions Architect in Elkin, NC.", }; render_html(&state, "home.html", context! { page, latest_post, random_blog_posts }, &request)}async fn blog_index( State(state): State<AppState>, uri: Uri, headers: HeaderMap,) -> Result<Html<String>, AppError> { let request = build_request(&uri, &headers); let posts = published_posts(&state); let tags = collect_tags(&posts); let years = collect_years(&posts); let breadcrumbs = vec![Crumb { title: "Home".into(), url: "/".into() }]; let page = context! { title => "Blog", slug => "blog", description => "Posts on webdev, coding, security, and sysadmin by Isaac Bythewood.", }; render_html( &state, "blog_index.html", context! { page, blog_posts => posts, tags, years, breadcrumbs }, &request, )}async fn blog_post( State(state): State<AppState>, AxumPath(slug): AxumPath<String>, uri: Uri, headers: HeaderMap,) -> Result<Html<String>, AppError> { let request = build_request(&uri, &headers); let idx = state.posts_by_slug.get(&slug).copied().ok_or_else(AppError::not_found)?; let post = state.posts[idx].clone(); if post.publish_date.as_str() > today().as_str() { return Err(AppError::not_found()); } let posts = published_posts(&state); let related_posts = related(&post, &posts, 3); let breadcrumbs = vec![ Crumb { title: "Home".into(), url: "/".into() }, Crumb { title: "Blog".into(), url: "/blog/".into() }, ]; render_html( &state, "blog_post.html", context! { page => &post, post => &post, related_posts, breadcrumbs }, &request, )}async fn blog_post_pdf( State(state): State<AppState>, AxumPath(slug): AxumPath<String>, uri: Uri, headers: HeaderMap,) -> Result<Response, AppError> { let request = build_request(&uri, &headers); let idx = state.posts_by_slug.get(&slug).copied().ok_or_else(AppError::not_found)?; let post = state.posts[idx].clone(); if post.publish_date.as_str() > today().as_str() { return Err(AppError::not_found()); } let tmpl = state.env.get_template("blog_post_pdf.html")?; let html = tmpl.render(context! { post => &post, request => &request })?; let server_base = state.server_base.clone(); let pdf = tokio::task::spawn_blocking(move || pdf::html_to_pdf(&html, &server_base)) .await .map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))??; let mut h = HeaderMap::new(); h.insert(header::CONTENT_TYPE, "application/pdf".parse().unwrap()); h.insert( header::CONTENT_DISPOSITION, format!("filename=\"{}.pdf\"", post.slug).parse().unwrap(), ); Ok((StatusCode::OK, h, Body::from(pdf)).into_response())}async fn blog_post_md( State(state): State<AppState>, AxumPath(slug): AxumPath<String>,) -> Result<Response, AppError> { let idx = state.posts_by_slug.get(&slug).copied().ok_or_else(AppError::not_found)?; let post = state.posts[idx].clone(); if post.publish_date.as_str() > today().as_str() { return Err(AppError::not_found()); } let path = state.content_dir.join("posts").join(&post.filename); let bytes = tokio::fs::read(&path).await?; let mut h = HeaderMap::new(); h.insert(header::CONTENT_TYPE, "text/markdown".parse().unwrap()); h.insert( header::CONTENT_DISPOSITION, format!("filename=\"{}.md\"", post.slug).parse().unwrap(), ); Ok((StatusCode::OK, h, Body::from(bytes)).into_response())}async fn blog_post_redirect(AxumPath(slug): AxumPath<String>) -> Redirect { Redirect::permanent(&format!("/posts/{slug}/"))}async fn blog_post_pdf_redirect(AxumPath(slug): AxumPath<String>) -> Redirect { Redirect::permanent(&format!("/posts/{slug}/pdf/"))}async fn blog_post_md_redirect(AxumPath(slug): AxumPath<String>) -> Redirect { Redirect::permanent(&format!("/posts/{slug}/md/"))}async fn blog_tag( State(state): State<AppState>, AxumPath(tag): AxumPath<String>, uri: Uri, headers: HeaderMap,) -> Result<Html<String>, AppError> { let request = build_request(&uri, &headers); let posts = published_posts(&state); let filtered: Vec<Post> = posts.iter().filter(|p| p.tags.contains(&tag)).cloned().collect(); if filtered.is_empty() { return Err(AppError::not_found()); } let extra_posts: Option<Vec<Post>> = if filtered.len() < 5 { Some(posts.iter().filter(|p| !p.tags.contains(&tag)).take(4).cloned().collect()) } else { None }; let tags = collect_tags(&posts); let years = collect_years(&posts); let active_tag = context! { name => &tag, slug => &tag }; let page = context! { title => format!("Tag: {tag}"), slug => format!("tag-{tag}"), description => format!("Posts tagged {tag}"), }; let breadcrumbs = vec![ Crumb { title: "Home".into(), url: "/".into() }, Crumb { title: "Blog".into(), url: "/blog/".into() }, ]; render_html( &state, "blog_index.html", context! { page, blog_posts => filtered, extra_posts, active_tag, tags, years, breadcrumbs }, &request, )}async fn blog_year( State(state): State<AppState>, AxumPath(year): AxumPath<String>, uri: Uri, headers: HeaderMap,) -> Result<Html<String>, AppError> { let request = build_request(&uri, &headers); let posts = published_posts(&state); let filtered: Vec<Post> = posts.iter().filter(|p| p.date.starts_with(&year)).cloned().collect(); if filtered.is_empty() { return Err(AppError::not_found()); } let extra_posts: Option<Vec<Post>> = if filtered.len() < 5 { Some(posts.iter().filter(|p| !p.date.starts_with(&year)).take(4).cloned().collect()) } else { None }; let tags = collect_tags(&posts); let years = collect_years(&posts); let page = context! { title => format!("Year: {year}"), slug => format!("year-{year}"), description => format!("Posts from {year}"), }; let breadcrumbs = vec![ Crumb { title: "Home".into(), url: "/".into() }, Crumb { title: "Blog".into(), url: "/blog/".into() }, ]; render_html( &state, "blog_index.html", context! { page, blog_posts => filtered, extra_posts, active_year => &year, tags, years, breadcrumbs }, &request, )}#[derive(Deserialize)]struct SearchQuery { #[serde(default)] q: String,}async fn search_page( State(state): State<AppState>, Query(q): Query<SearchQuery>, uri: Uri, headers: HeaderMap,) -> Result<Html<String>, AppError> { let request = build_request(&uri, &headers); let posts = published_posts(&state); let mut results: Vec<Post> = Vec::new(); let mut random_posts: Option<Vec<Post>> = None; if !q.q.is_empty() { let ql = q.q.to_lowercase(); for p in &posts { if p.title.to_lowercase().contains(&ql) || p.description.to_lowercase().contains(&ql) || p.tags.iter().any(|t| t.to_lowercase().contains(&ql)) { results.push(p.clone()); } } } else { let mut rng = rand::thread_rng(); let mut shuffled = posts.clone(); shuffled.shuffle(&mut rng); random_posts = Some(shuffled.into_iter().take(6).collect()); } let breadcrumbs = vec![Crumb { title: "Home".into(), url: "/".into() }]; let page = context! { title => "Search", slug => "search", description => "Search posts on webdev, coding, security, and sysadmin.", }; render_html( &state, "search.html", context! { page, results, random_posts, q => &q.q, breadcrumbs }, &request, )}async fn search_live( State(state): State<AppState>, Query(q): Query<SearchQuery>,) -> Response { let posts = published_posts(&state); let mut out = Vec::new(); if !q.q.is_empty() { let ql = q.q.to_lowercase(); for p in &posts { if p.title.to_lowercase().contains(&ql) || p.description.to_lowercase().contains(&ql) || p.tags.iter().any(|t| t.to_lowercase().contains(&ql)) { out.push(serde_json::json!({ "title": p.title, "description": p.description, "url": format!("/posts/{}/", p.slug), })); if out.len() >= 5 { break; } } } } // Match Flask's jsonify: trailing newline. let body = serde_json::to_string(&out).unwrap_or_default() + "\n"; let mut h = HeaderMap::new(); h.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); (StatusCode::OK, h, body).into_response()}async fn og_image( State(state): State<AppState>, AxumPath(slug_svg): AxumPath<String>, uri: Uri, headers: HeaderMap,) -> Result<Response, AppError> { let request = build_request(&uri, &headers); let slug = slug_svg.strip_suffix(".svg").unwrap_or(&slug_svg).to_string(); let (title, tags) = match state.posts_by_slug.get(&slug).copied() { Some(idx) => (state.posts[idx].title.clone(), state.posts[idx].tags.clone()), None => ("Isaac Bythewood's Blog".to_string(), Vec::new()), }; let mut lines: Vec<String> = Vec::new(); let mut current = String::new(); for word in title.split_whitespace() { if !current.is_empty() && current.len() + word.len() + 1 > 35 { lines.push(current); current = word.to_string(); } else if current.is_empty() { current = word.to_string(); } else { current.push(' '); current.push_str(word); } } if !current.is_empty() { lines.push(current); } let title_lines: Vec<String> = lines.into_iter().take(3).collect(); let tmpl = state.env.get_template("og.svg")?; let body = tmpl.render(context! { title_lines, tags, request => &request })?; let mut h = HeaderMap::new(); h.insert(header::CONTENT_TYPE, "image/svg+xml".parse().unwrap()); Ok((StatusCode::OK, h, body).into_response())}async fn favicon(State(state): State<AppState>) -> Result<Response, AppError> { let tmpl = state.env.get_template("favicon.svg")?; let body = tmpl.render(Value::UNDEFINED)?; let mut h = HeaderMap::new(); h.insert(header::CONTENT_TYPE, "image/svg+xml".parse().unwrap()); Ok((StatusCode::OK, h, body).into_response())}async fn robots( State(state): State<AppState>, uri: Uri, headers: HeaderMap,) -> Result<Response, AppError> { let request = build_request(&uri, &headers); let tmpl = state.env.get_template("robots.txt")?; let body = tmpl.render(context! { request => &request })?; let mut h = HeaderMap::new(); h.insert(header::CONTENT_TYPE, "text/plain".parse().unwrap()); Ok((StatusCode::OK, h, body).into_response())}async fn sitemap( State(state): State<AppState>, uri: Uri, headers: HeaderMap,) -> Result<Response, AppError> { let request = build_request(&uri, &headers); let posts = published_posts(&state); let tags = collect_tags(&posts); let years = collect_years(&posts); let mut tag_lastmod: HashMap<String, String> = HashMap::new(); let mut year_lastmod: HashMap<String, String> = HashMap::new(); for p in &posts { for t in &p.tags { tag_lastmod .entry(t.clone()) .and_modify(|cur| { if p.date > *cur { *cur = p.date.clone(); } }) .or_insert_with(|| p.date.clone()); } if p.date.len() >= 4 { let y = p.date[..4].to_string(); year_lastmod .entry(y) .and_modify(|cur| { if p.date > *cur { *cur = p.date.clone(); } }) .or_insert_with(|| p.date.clone()); } } let tmpl = state.env.get_template("sitemap.xml")?; let body = tmpl.render(context! { posts, tags, years, tag_lastmod, year_lastmod, request => &request, })?; let mut h = HeaderMap::new(); h.insert(header::CONTENT_TYPE, "application/xml".parse().unwrap()); Ok((StatusCode::OK, h, body).into_response())}async fn not_found( State(state): State<AppState>, uri: Uri, headers: HeaderMap,) -> Response { let request = build_request(&uri, &headers); let page = context! { title => "404", description => "Page not found" }; match render_html(&state, "404.html", context! { page }, &request) { Ok(html) => (StatusCode::NOT_FOUND, html).into_response(), Err(_) => (StatusCode::NOT_FOUND, "404 Not Found").into_response(), }}
@@ -0,0 +1,247 @@use comrak::{ nodes::{AstNode, NodeValue}, Arena, ComrakOptions,};use std::cell::RefCell;use std::fmt::Write;fn options() -> ComrakOptions { let mut opts = ComrakOptions::default(); opts.extension.strikethrough = true; opts.render.unsafe_ = true; opts}pub fn render_blog(md: &str) -> String { let arena = Arena::new(); let opts = options(); let root = comrak::parse_document(&arena, md, &opts); let mut out = String::with_capacity(md.len() * 2); render_node(root, &mut out, &opts, false); out}pub fn render_pdf(md: &str) -> String { let arena = Arena::new(); let opts = options(); let root = comrak::parse_document(&arena, md, &opts); let mut out = String::with_capacity(md.len() * 2); render_node(root, &mut out, &opts, true); out}fn render_node<'a>(node: &'a AstNode<'a>, out: &mut String, opts: &ComrakOptions, pdf: bool) { let value = &node.data.borrow().value; match value { NodeValue::Document => { for child in node.children() { render_node(child, out, opts, pdf); } } NodeValue::Paragraph => { // Handle the BlogRenderer paragraph -> block-image short-circuit: // if the paragraph contains exactly one image, render it as block-image // without wrapping in block-rich-text. let only_image = is_paragraph_only_image(node); if only_image { for child in node.children() { render_inline(child, out, opts); } } else { out.push_str("<div class=\"block-rich-text\"><p>"); for child in node.children() { render_inline(child, out, opts); } out.push_str("</p></div>\n"); } } NodeValue::Heading(h) => { let level = h.level; write!(out, "<div class=\"block-rich-text\"><h{level}>").ok(); for child in node.children() { render_inline(child, out, opts); } write!(out, "</h{level}></div>\n").ok(); } NodeValue::List(_) => { let ordered = matches!( value, NodeValue::List(l) if l.list_type == comrak::nodes::ListType::Ordered ); let tag = if ordered { "ol" } else { "ul" }; write!(out, "<div class=\"block-rich-text\"><{tag}>").ok(); for child in node.children() { render_node(child, out, opts, pdf); } write!(out, "</{tag}></div>\n").ok(); } NodeValue::Item(_) => { out.push_str("<li>"); for child in node.children() { // Inside list items, render children directly without re-wrapping // (paragraph children become inline text). match &child.data.borrow().value { NodeValue::Paragraph => { for c in child.children() { render_inline(c, out, opts); } } _ => render_node(child, out, opts, pdf), } } out.push_str("</li>"); } NodeValue::BlockQuote => { out.push_str("<div class=\"block-rich-text\"><blockquote>"); for child in node.children() { render_node(child, out, opts, pdf); } out.push_str("</blockquote></div>\n"); } NodeValue::ThematicBreak => { out.push_str("<div class=\"block-rich-text\"><hr></div>\n"); } NodeValue::CodeBlock(c) => { let mut lang = c.info.split_whitespace().next().unwrap_or("").to_string(); if !pdf && lang == "html" { lang = "htmlmixed".to_string(); } let escaped = html_escape(&c.literal); if pdf { write!( out, "<div class=\"block-code\"><pre><code class=\"language-{lang}\">{escaped}</code></pre></div>\n" ) .ok(); } else { write!( out, "<div class=\"block-code\"><textarea data-language=\"{lang}\">{escaped}</textarea></div>\n" ) .ok(); } } NodeValue::HtmlBlock(h) => { out.push_str(&h.literal); } _ => { // Fallback: emit raw HTML for unhandled block types via comrak let buf = RefCell::new(String::new()); // For simplicity, just iterate children for child in node.children() { render_node(child, out, opts, pdf); } drop(buf); } }}fn is_paragraph_only_image<'a>(para: &'a AstNode<'a>) -> bool { let mut iter = para.children(); let first = iter.next(); let second = iter.next(); if second.is_some() { return false; } match first { Some(child) => matches!(child.data.borrow().value, NodeValue::Image(_)), None => false, }}fn render_inline<'a>(node: &'a AstNode<'a>, out: &mut String, opts: &ComrakOptions) { let value = &node.data.borrow().value; match value { NodeValue::Text(t) => out.push_str(&html_escape(t)), NodeValue::SoftBreak => out.push('\n'), NodeValue::LineBreak => out.push_str("<br>\n"), NodeValue::Code(c) => { write!(out, "<code>{}</code>", html_escape(&c.literal)).ok(); } NodeValue::HtmlInline(s) => out.push_str(s), NodeValue::Emph => { out.push_str("<em>"); for child in node.children() { render_inline(child, out, opts); } out.push_str("</em>"); } NodeValue::Strong => { out.push_str("<strong>"); for child in node.children() { render_inline(child, out, opts); } out.push_str("</strong>"); } NodeValue::Strikethrough => { out.push_str("<del>"); for child in node.children() { render_inline(child, out, opts); } out.push_str("</del>"); } NodeValue::Link(l) => { write!(out, "<a href=\"{}\"", html_escape(&l.url)).ok(); if !l.title.is_empty() { write!(out, " title=\"{}\"", html_escape(&l.title)).ok(); } out.push_str(">"); for child in node.children() { render_inline(child, out, opts); } out.push_str("</a>"); } NodeValue::Image(l) => { // BlogRenderer.image: strip "images/" prefix, wrap in block-image div let mut url = l.url.clone(); if let Some(stripped) = url.strip_prefix("images/") { url = stripped.to_string(); } // The "alt text" in comrak is the children rendered as plain text let mut alt = String::new(); for child in node.children() { collect_text(child, &mut alt); } write!( out, "<div class=\"block-image\"><img src=\"/content/images/{}\" class=\"rounded\" alt=\"{}\"></div>\n", html_escape(&url), html_escape(&alt), ) .ok(); } _ => { // Fallback: just render children inline for child in node.children() { render_inline(child, out, opts); } } }}fn collect_text<'a>(node: &'a AstNode<'a>, buf: &mut String) { match &node.data.borrow().value { NodeValue::Text(t) => buf.push_str(t), NodeValue::Code(c) => buf.push_str(&c.literal), _ => { for child in node.children() { collect_text(child, buf); } } }}// Match Mistune's escape: & < > " (no apostrophe).fn html_escape(s: &str) -> String { let mut out = String::with_capacity(s.len()); for c in s.chars() { match c { '&' => out.push_str("&"), '<' => out.push_str("<"), '>' => out.push_str(">"), '"' => out.push_str("""), _ => out.push(c), } } out}
@@ -0,0 +1,106 @@use std::path::PathBuf;use std::process::Command;use tempfile::{Builder, NamedTempFile};/// Render an HTML string to a PDF using headless Chromium.////// Spawns chrome-headless-shell with --print-to-pdf. The HTML is written to a/// tempfile and loaded as a file:// URL so relative URLs (e.g. /content/images/...)/// can be rewritten to absolute http:// URLs pointing at the live server.pub fn html_to_pdf(html: &str, server_base: &str) -> anyhow::Result<Vec<u8>> { // Rewrite relative absolute paths (/content/, /static/) to point at the // running server so chromium can fetch them. let rewritten = html .replace("src=\"/content/", &format!("src=\"{server_base}/content/")) .replace("href=\"/content/", &format!("href=\"{server_base}/content/")) .replace("src=\"/static/", &format!("src=\"{server_base}/static/")) .replace("href=\"/static/", &format!("href=\"{server_base}/static/")); // Suffix matters: chromium decides HTML vs plain-text rendering by extension. // Without .html the page is shown as raw source text instead of being rendered. let mut html_file = Builder::new().suffix(".html").tempfile()?; { use std::io::Write; html_file.write_all(rewritten.as_bytes())?; html_file.flush()?; } let html_path = html_file.path().to_path_buf(); let pdf_file = NamedTempFile::new()?; let pdf_path = pdf_file.path().to_path_buf(); let url = format!("file://{}", html_path.display()); let print_arg = format!("--print-to-pdf={}", pdf_path.display()); run_chromium(&url, &print_arg)?; let bytes = std::fs::read(&pdf_path)?; Ok(bytes)}fn run_chromium(url: &str, print_arg: &str) -> anyhow::Result<()> { let bin = find_chromium().ok_or_else(|| { anyhow::anyhow!("could not locate chromium; set CHROMIUM_BIN or install chromium on PATH") })?; let output = Command::new(&bin) .arg("--headless=new") .arg("--no-sandbox") .arg("--disable-gpu") .arg("--no-pdf-header-footer") .arg("--hide-scrollbars") .arg(print_arg) .arg(url) .output()?; if !output.status.success() { anyhow::bail!( "chromium ({}) exited {}: {}", bin.display(), output.status, String::from_utf8_lossy(&output.stderr) ); } Ok(())}/// Locate a chromium binary across environments:/// 1. `CHROMIUM_BIN` env var (explicit override)/// 2. PATH search for common chromium binary names (production install)/// 3. Glob under `/opt/playwright-browsers/` (dev container with Playwright)fn find_chromium() -> Option<PathBuf> { if let Ok(p) = std::env::var("CHROMIUM_BIN") { let path = PathBuf::from(p); if path.is_file() { return Some(path); } } let names = [ "chromium", "chromium-browser", "chrome-headless-shell", "google-chrome", "chrome", ]; if let Some(path_var) = std::env::var_os("PATH") { for dir in std::env::split_paths(&path_var) { for name in &names { let candidate = dir.join(name); if candidate.is_file() { return Some(candidate); } } } } if let Ok(entries) = std::fs::read_dir("/opt/playwright-browsers") { for entry in entries.flatten() { let candidate = entry .path() .join("chrome-headless-shell-linux64/chrome-headless-shell"); if candidate.is_file() { return Some(candidate); } } } None}
@@ -0,0 +1,104 @@use serde::Serialize;use std::collections::HashMap;use std::fs;use std::path::PathBuf;use crate::markdown;#[derive(Debug, Clone, Serialize)]pub struct Post { pub filename: String, pub title: String, pub slug: String, pub date: String, pub publish_date: String, pub tags: Vec<String>, pub description: String, pub cover_image: String, pub body_html: String, pub body_html_pdf: String, pub read_time: usize,}pub fn parse_frontmatter(text: &str) -> (HashMap<String, String>, &str) { let mut meta = HashMap::new(); if !text.starts_with("---") { return (meta, text); } let after_first = &text[3..]; let end_rel = match after_first.find("---") { Some(e) => e, None => return (meta, text), }; let block = &after_first[..end_rel]; let body_start = 3 + end_rel + 3; let body = text[body_start..].trim_start_matches(['\r', '\n', ' ', '\t']); for line in block.trim().lines() { if let Some((k, v)) = line.split_once(": ") { meta.insert(k.trim().to_string(), v.trim().to_string()); } } (meta, body)}pub fn load_posts(content_dir: &PathBuf) -> Vec<Post> { let posts_dir = content_dir.join("posts"); let mut posts = Vec::new(); let entries = match fs::read_dir(&posts_dir) { Ok(e) => e, Err(_) => return posts, }; for entry in entries.flatten() { let path = entry.path(); if path.extension().and_then(|s| s.to_str()) != Some("md") { continue; } let filename = path .file_name() .and_then(|s| s.to_str()) .unwrap_or("") .to_string(); let text = match fs::read_to_string(&path) { Ok(t) => t, Err(_) => continue, }; let (meta, body) = parse_frontmatter(&text); let tags: Vec<String> = meta .get("tags") .map(|s| { s.split(',') .map(|t| t.trim().to_string()) .filter(|t| !t.is_empty()) .collect() }) .unwrap_or_default(); let date = meta.get("date").cloned().unwrap_or_default(); let publish_date = meta.get("publish_date").cloned().unwrap_or_else(|| date.clone()); let body_html = markdown::render_blog(body); let body_html_pdf = markdown::render_pdf(body); let word_count = body.split_whitespace().count(); let read_time = ((word_count as f64) / 200.0).ceil() as usize; let read_time = read_time.max(1); let slug = meta .get("slug") .cloned() .unwrap_or_else(|| filename.trim_end_matches(".md").to_string()); posts.push(Post { filename, title: meta.get("title").cloned().unwrap_or_default(), slug, date, publish_date, tags, description: meta.get("description").cloned().unwrap_or_default(), cover_image: meta.get("cover_image").cloned().unwrap_or_default(), body_html, body_html_pdf, read_time, }); } posts.sort_by(|a, b| b.date.cmp(&a.date)); posts}
@@ -0,0 +1,202 @@use minijinja::value::{Kwargs, Value};use minijinja::{path_loader, AutoEscape, Environment, Error, ErrorKind, Output, State};use serde::Serialize;use serde_json::Value as JsonValue;use std::path::Path;/// Custom formatter that matches Jinja2's HTML escape (does NOT escape `/`).fn jinja2_html_formatter( out: &mut Output, _state: &State, value: &Value,) -> Result<(), Error> { if value.is_safe() { write!(out, "{value}").map_err(Error::from)?; return Ok(()); } let auto_escape = match _state.auto_escape() { AutoEscape::Html => true, AutoEscape::None => false, _ => return minijinja::escape_formatter(out, _state, value), }; if !auto_escape { write!(out, "{value}").map_err(Error::from)?; return Ok(()); } if let Some(s) = value.as_str() { write_jinja2_html(out, s).map_err(Error::from)?; } else if value.is_undefined() || value.is_none() { // emit nothing } else { let stringified = value.to_string(); write_jinja2_html(out, &stringified).map_err(Error::from)?; } Ok(())}fn write_jinja2_html(out: &mut Output, s: &str) -> std::fmt::Result { let mut last = 0; for (i, b) in s.bytes().enumerate() { let escape = match b { b'&' => "&", b'<' => "<", b'>' => ">", b'"' => """, b'\'' => "'", _ => continue, }; if last < i { out.write_str(&s[last..i])?; } out.write_str(escape)?; last = i + 1; } if last < s.len() { out.write_str(&s[last..])?; } Ok(())}#[derive(Debug, Clone, Serialize)]pub struct RequestCtx { pub url: String, pub url_root: String, pub base_url: String,}fn read_manifest(path: &Path) -> JsonValue { let text = std::fs::read_to_string(path).unwrap_or_else(|_| "{}".to_string()); serde_json::from_str(&text).unwrap_or(JsonValue::Null)}fn lookup_asset(manifest: &JsonValue, entry: &str, kind: &str) -> String { if let Some(chunk) = manifest.get(entry) { if kind == "css" { if let Some(css_arr) = chunk.get("css").and_then(|v| v.as_array()) { if let Some(first) = css_arr.first().and_then(|v| v.as_str()) { return format!("/static/{first}"); } } } if let Some(file) = chunk.get("file").and_then(|v| v.as_str()) { return format!("/static/{file}"); } } format!("/static/{entry}")}pub fn build_env(templates_dir: &Path, manifest_path: &Path) -> Environment<'static> { let mut env = Environment::new(); env.set_loader(path_loader(templates_dir)); env.set_formatter(jinja2_html_formatter); // Vite manifest: // - debug builds re-read on every call so Vite watcher rebuilds show up immediately // - release builds load once at startup and reuse the cached value #[cfg(debug_assertions)] { let path = manifest_path.to_path_buf(); env.add_function( "vite_asset", move |entry: String, kind: Option<String>| -> Result<String, Error> { let kind = kind.unwrap_or_else(|| "file".to_string()); let manifest = read_manifest(&path); Ok(lookup_asset(&manifest, &entry, &kind)) }, ); } #[cfg(not(debug_assertions))] { let manifest = read_manifest(manifest_path); env.add_function( "vite_asset", move |entry: String, kind: Option<String>| -> Result<String, Error> { let kind = kind.unwrap_or_else(|| "file".to_string()); Ok(lookup_asset(&manifest, &entry, &kind)) }, ); } env.add_function("url_for", url_for); env}fn url_for(state: &State, endpoint: String, kwargs: Kwargs) -> Result<String, Error> { let take_str = |k: &str| -> Result<Option<String>, Error> { let v: Option<Value> = kwargs.get(k).ok(); match v { None => Ok(None), Some(val) => { if val.is_undefined() || val.is_none() { Ok(None) } else { Ok(Some(val.to_string())) } } } }; let external: bool = kwargs.get("_external").unwrap_or(false); let path = match endpoint.as_str() { "index" => "/".to_string(), "blog_index" => "/blog/".to_string(), "blog_post" => { let slug = take_str("slug")?.unwrap_or_default(); format!("/posts/{}/", urlencoding::encode(&slug)) } "blog_post_pdf" => { let slug = take_str("slug")?.unwrap_or_default(); format!("/posts/{}/pdf/", urlencoding::encode(&slug)) } "blog_post_md" => { let slug = take_str("slug")?.unwrap_or_default(); format!("/posts/{}/md/", urlencoding::encode(&slug)) } "blog_tag" => { let tag = take_str("tag")?.unwrap_or_default(); format!("/blog/tag/{}/", urlencoding::encode(&tag)) } "blog_year" => { let year = take_str("year")?.unwrap_or_default(); format!("/blog/year/{}/", urlencoding::encode(&year)) } "content_images" => { let filename = take_str("filename")?.unwrap_or_default(); format!("/content/images/{filename}") } "og_image" => { let slug = take_str("slug")?.unwrap_or_default(); format!("/og/{slug}.svg") } "search" => "/search/".to_string(), "static" => { let filename = take_str("filename")?.unwrap_or_default(); format!("/static/{filename}") } other => { return Err(Error::new( ErrorKind::InvalidOperation, format!("unknown route in url_for: {other}"), )); } }; // Allow assert_all_used to pass with our optional kwargs. kwargs.assert_all_used()?; if external { let url_root: Option<String> = state .lookup("request") .and_then(|req| req.get_attr("url_root").ok()) .and_then(|v| if v.is_undefined() { None } else { Some(v.to_string()) }); if let Some(mut root) = url_root { if root.ends_with('/') { root.pop(); } return Ok(format!("{root}{path}")); } } Ok(path)}
modified
templates/includes/social.html
@@ -3,11 +3,11 @@<meta property="og:title" content="{{ page.title }}"><meta property="og:description" content="{{ page.description }}"><meta property="og:url" content="{{ request.url }}"><meta property="og:image" content="{{ url_for('og_image', slug=page.slug if page.slug is defined else 'blog', _external=True) }}"><meta property="og:image" content="{{ url_for('og_image', slug=(page.slug if page.slug is defined else 'blog'), _external=True) }}"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="{{ page.title }}"><meta name="twitter:description" content="{{ page.description }}"><meta name="twitter:image" content="{{ url_for('og_image', slug=page.slug if page.slug is defined else 'blog', _external=True) }}"><meta name="twitter:image" content="{{ url_for('og_image', slug=(page.slug if page.slug is defined else 'blog'), _external=True) }}">
@@ -1,483 +0,0 @@version = 1revision = 3requires-python = ">=3.13"[[package]]name = "blinker"version = "1.9.0"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },][[package]]name = "blog-bythewood-me"version = "0.1.0"source = { virtual = "." }dependencies = [ { name = "flask" }, { name = "gunicorn" }, { name = "mistune" }, { name = "weasyprint" },][package.metadata]requires-dist = [ { name = "flask", specifier = ">=3.1" }, { name = "gunicorn", specifier = ">=23.0" }, { name = "mistune", specifier = ">=3.1" }, { name = "weasyprint", specifier = ">=68.1" },][[package]]name = "brotli"version = "1.2.0"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" }, { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" }, { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" }, { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" }, { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" }, { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" }, { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" }, { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" }, { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" }, { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" }, { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" }, { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" }, { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" }, { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" }, { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" }, { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" }, { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" }, { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" }, { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" },][[package]]name = "brotlicffi"version = "1.2.0.1"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "cffi" },]sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/ef/f9/dfa56316837fa798eac19358351e974de8e1e2ca9475af4cb90293cd6576/brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd", size = 433046, upload-time = "2026-03-05T19:53:46.209Z" }, { url = "https://files.pythonhosted.org/packages/4a/f5/f8f492158c76b0d940388801f04f747028971ad5774287bded5f1e53f08d/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5", size = 1541126, upload-time = "2026-03-05T19:53:48.248Z" }, { url = "https://files.pythonhosted.org/packages/3b/e1/ff87af10ac419600c63e9287a0649c673673ae6b4f2bcf48e96cb2f89f60/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac", size = 1541983, upload-time = "2026-03-05T19:53:50.317Z" }, { url = "https://files.pythonhosted.org/packages/47/c0/80ecd9bd45776109fab14040e478bf63e456967c9ddee2353d8330ed8de1/brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec", size = 349047, upload-time = "2026-03-05T19:53:52.215Z" }, { url = "https://files.pythonhosted.org/packages/ab/98/13e5b250236a281b6cd9e92a01ee1ae231029fa78faee932ef3766e1cb24/brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000", size = 385652, upload-time = "2026-03-05T19:53:53.892Z" }, { url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" }, { url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" }, { url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" }, { url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" }, { url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" },][[package]]name = "cffi"version = "2.0.0"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" },]sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },][[package]]name = "click"version = "8.3.2"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" },]sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },][[package]]name = "colorama"version = "0.4.6"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },][[package]]name = "cssselect2"version = "0.9.0"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "tinycss2" }, { name = "webencodings" },]sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" },][[package]]name = "flask"version = "3.1.3"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "blinker" }, { name = "click" }, { name = "itsdangerous" }, { name = "jinja2" }, { name = "markupsafe" }, { name = "werkzeug" },]sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },][[package]]name = "fonttools"version = "4.62.1"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/9a/08/7012b00a9a5874311b639c3920270c36ee0c445b69d9989a85e5c92ebcb0/fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d", size = 3580737, upload-time = "2026-03-13T13:54:25.52Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/3b/56/6f389de21c49555553d6a5aeed5ac9767631497ac836c4f076273d15bd72/fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79", size = 2865155, upload-time = "2026-03-13T13:53:16.132Z" }, { url = "https://files.pythonhosted.org/packages/03/c5/0e3966edd5ec668d41dfe418787726752bc07e2f5fd8c8f208615e61fa89/fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe", size = 2412802, upload-time = "2026-03-13T13:53:18.878Z" }, { url = "https://files.pythonhosted.org/packages/52/94/e6ac4b44026de7786fe46e3bfa0c87e51d5d70a841054065d49cd62bb909/fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68", size = 5013926, upload-time = "2026-03-13T13:53:21.379Z" }, { url = "https://files.pythonhosted.org/packages/e2/98/8b1e801939839d405f1f122e7d175cebe9aeb4e114f95bfc45e3152af9a7/fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1", size = 4964575, upload-time = "2026-03-13T13:53:23.857Z" }, { url = "https://files.pythonhosted.org/packages/46/76/7d051671e938b1881670528fec69cc4044315edd71a229c7fd712eaa5119/fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069", size = 4953693, upload-time = "2026-03-13T13:53:26.569Z" }, { url = "https://files.pythonhosted.org/packages/1f/ae/b41f8628ec0be3c1b934fc12b84f4576a5c646119db4d3bdd76a217c90b5/fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9", size = 5094920, upload-time = "2026-03-13T13:53:29.329Z" }, { url = "https://files.pythonhosted.org/packages/f2/f6/53a1e9469331a23dcc400970a27a4caa3d9f6edbf5baab0260285238b884/fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24", size = 2279928, upload-time = "2026-03-13T13:53:32.352Z" }, { url = "https://files.pythonhosted.org/packages/38/60/35186529de1db3c01f5ad625bde07c1f576305eab6d86bbda4c58445f721/fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056", size = 2330514, upload-time = "2026-03-13T13:53:34.991Z" }, { url = "https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca", size = 2864442, upload-time = "2026-03-13T13:53:37.509Z" }, { url = "https://files.pythonhosted.org/packages/4b/b2/e521803081f8dc35990816b82da6360fa668a21b44da4b53fc9e77efcd62/fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca", size = 2410901, upload-time = "2026-03-13T13:53:40.55Z" }, { url = "https://files.pythonhosted.org/packages/00/a4/8c3511ff06e53110039358dbbdc1a65d72157a054638387aa2ada300a8b8/fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782", size = 4999608, upload-time = "2026-03-13T13:53:42.798Z" }, { url = "https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae", size = 4912726, upload-time = "2026-03-13T13:53:45.405Z" }, { url = "https://files.pythonhosted.org/packages/70/b9/ac677cb07c24c685cf34f64e140617d58789d67a3dd524164b63648c6114/fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7", size = 4951422, upload-time = "2026-03-13T13:53:48.326Z" }, { url = "https://files.pythonhosted.org/packages/e6/10/11c08419a14b85b7ca9a9faca321accccc8842dd9e0b1c8a72908de05945/fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a", size = 5060979, upload-time = "2026-03-13T13:53:51.366Z" }, { url = "https://files.pythonhosted.org/packages/4e/3c/12eea4a4cf054e7ab058ed5ceada43b46809fce2bf319017c4d63ae55bb4/fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800", size = 2283733, upload-time = "2026-03-13T13:53:53.606Z" }, { url = "https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e", size = 2335663, upload-time = "2026-03-13T13:53:56.23Z" }, { url = "https://files.pythonhosted.org/packages/42/c5/4d2ed3ca6e33617fc5624467da353337f06e7f637707478903c785bd8e20/fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82", size = 2947288, upload-time = "2026-03-13T13:53:59.397Z" }, { url = "https://files.pythonhosted.org/packages/1f/e9/7ab11ddfda48ed0f89b13380e5595ba572619c27077be0b2c447a63ff351/fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260", size = 2449023, upload-time = "2026-03-13T13:54:01.642Z" }, { url = "https://files.pythonhosted.org/packages/b2/10/a800fa090b5e8819942e54e19b55fc7c21fe14a08757c3aa3ca8db358939/fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4", size = 5137599, upload-time = "2026-03-13T13:54:04.495Z" }, { url = "https://files.pythonhosted.org/packages/37/dc/8ccd45033fffd74deb6912fa1ca524643f584b94c87a16036855b498a1ed/fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b", size = 4920933, upload-time = "2026-03-13T13:54:07.557Z" }, { url = "https://files.pythonhosted.org/packages/99/eb/e618adefb839598d25ac8136cd577925d6c513dc0d931d93b8af956210f0/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87", size = 5016232, upload-time = "2026-03-13T13:54:10.611Z" }, { url = "https://files.pythonhosted.org/packages/d9/5f/9b5c9bfaa8ec82def8d8168c4f13615990d6ce5996fe52bd49bfb5e05134/fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c", size = 5042987, upload-time = "2026-03-13T13:54:13.569Z" }, { url = "https://files.pythonhosted.org/packages/90/aa/dfbbe24c6a6afc5c203d90cc0343e24bcbb09e76d67c4d6eef8c2558d7ba/fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a", size = 2348021, upload-time = "2026-03-13T13:54:16.98Z" }, { url = "https://files.pythonhosted.org/packages/13/6f/ae9c4e4dd417948407b680855c2c7790efb52add6009aaecff1e3bc50e8e/fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e", size = 2414147, upload-time = "2026-03-13T13:54:19.416Z" }, { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" },][package.optional-dependencies]woff = [ { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, { name = "zopfli" },][[package]]name = "gunicorn"version = "25.3.0"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "packaging" },]sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" }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 = "itsdangerous"version = "2.2.0"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },][[package]]name = "jinja2"version = "3.1.6"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "markupsafe" },]sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },][[package]]name = "markupsafe"version = "3.0.3"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },][[package]]name = "mistune"version = "3.2.0"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" },][[package]]name = "packaging"version = "26.0"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },][[package]]name = "pillow"version = "12.2.0"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },][[package]]name = "pycparser"version = "3.0"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },][[package]]name = "pydyf"version = "0.12.1"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/36/ee/fb410c5c854b6a081a49077912a9765aeffd8e07cbb0663cfda310b01fb4/pydyf-0.12.1.tar.gz", hash = "sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095", size = 17716, upload-time = "2025-12-02T14:52:14.244Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/22/11/47efe2f66ba848a107adfd490b508f5c0cedc82127950553dca44d29e6c4/pydyf-0.12.1-py3-none-any.whl", hash = "sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc", size = 8028, upload-time = "2025-12-02T14:52:12.938Z" },][[package]]name = "pyphen"version = "0.17.2"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" },][[package]]name = "tinycss2"version = "1.5.1"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "webencodings" },]sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },][[package]]name = "tinyhtml5"version = "2.1.0"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "webencodings" },]sdist = { url = "https://files.pythonhosted.org/packages/b1/1f/cfe2f6b30557c92b3f31d41707e09cef5c1efbd87392bc6c0430c46b0e4d/tinyhtml5-2.1.0.tar.gz", hash = "sha256:60a50ec3d938a37e491efa01af895853060943dcebb5627de5b10d188b338a67", size = 179242, upload-time = "2026-03-05T17:06:30.704Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/52/48/01695a036b695f83fea7aef6955d735db0f517b1c8e25ddb399ac0bdbcbf/tinyhtml5-2.1.0-py3-none-any.whl", hash = "sha256:6e11cfff38515834268daf89d5f85bbde0b6dd02e8d9e212d1385c2289b89f0a", size = 39686, upload-time = "2026-03-05T17:06:28.498Z" },][[package]]name = "weasyprint"version = "68.1"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "cffi" }, { name = "cssselect2" }, { name = "fonttools", extra = ["woff"] }, { name = "pillow" }, { name = "pydyf" }, { name = "pyphen" }, { name = "tinycss2" }, { name = "tinyhtml5" },]sdist = { url = "https://files.pythonhosted.org/packages/db/3e/65c0f176e6fb5c2b0a1ac13185b366f727d9723541babfa7fa4309998169/weasyprint-68.1.tar.gz", hash = "sha256:d3b752049b453a5c95edb27ce78d69e9319af5a34f257fa0f4c738c701b4184e", size = 1542379, upload-time = "2026-02-06T15:04:11.203Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/dd/dd/14eb73cea481ad8162d3b18a4850d4a84d6e804a22840cca207648532265/weasyprint-68.1-py3-none-any.whl", hash = "sha256:4dc3ba63c68bbbce3e9617cb2226251c372f5ee90a8a484503b1c099da9cf5be", size = 319789, upload-time = "2026-02-06T15:04:09.189Z" },][[package]]name = "webencodings"version = "0.5.1"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },][[package]]name = "werkzeug"version = "3.1.8"source = { registry = "https://pypi.org/simple" }dependencies = [ { name = "markupsafe" },]sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },][[package]]name = "zopfli"version = "0.4.1"source = { registry = "https://pypi.org/simple" }sdist = { url = "https://files.pythonhosted.org/packages/0a/4d/a8cc1768b2eda3c0c7470bf8059dcb94ef96d45dd91fc6edd29430d44072/zopfli-0.4.1.tar.gz", hash = "sha256:07a5cdc5d1aaa6c288c5d9f5a5383042ba743641abf8e2fd898dcad622d8a38e", size = 179001, upload-time = "2026-02-13T14:17:27.156Z" }wheels = [ { url = "https://files.pythonhosted.org/packages/e1/2f/1a7082e9163ae3703b27d571720bf3c954a02a9cf1fdce47c51e70639256/zopfli-0.4.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:4238d4d746d1095e29c9125490985e0c12ffd3654f54a24af551e2391e936d54", size = 291570, upload-time = "2026-02-13T14:17:12.556Z" }, { url = "https://files.pythonhosted.org/packages/dd/6f/4a1a88edf9fa0ce102703f38ab4dfb285b7cd2dde5389184264ec759e06e/zopfli-0.4.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fdfb7ce9f5de37a5b2f75dd2642fd7717956ef2a72e0387302a36d382440db07", size = 829437, upload-time = "2026-02-13T14:17:14.431Z" }, { url = "https://files.pythonhosted.org/packages/e3/77/d231012ddcaac9d2e184bd7808e106a8a0048855912e2e1c902b3f383413/zopfli-0.4.1-cp310-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7bcee1b189d64ec33d1e05cfa1b6a1268c29329c382f6ca1bd6245b04925c57", size = 818542, upload-time = "2026-02-13T14:17:16.353Z" }, { url = "https://files.pythonhosted.org/packages/0d/4e/9b23690c4ca14fbeae2a8f7f6b2006611bf4cd7d5bcb2d9e6c718bd4b0e9/zopfli-0.4.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:27823dc1161a4031d1c25925fd45d9868ec0cbc7692341830a7dcfa25063662c", size = 1778034, upload-time = "2026-02-13T14:17:17.509Z" }, { url = "https://files.pythonhosted.org/packages/e3/1b/51f7c28d4cde639cac4f5d47ff615548c1d9809f43cbacdd66eba5cd679d/zopfli-0.4.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a4c22b6161f47f5bd34637dbaee6735abd287cd64e0d1ce28ef1871bf625f4b", size = 1863957, upload-time = "2026-02-13T14:17:19.259Z" }, { url = "https://files.pythonhosted.org/packages/ae/4d/1ef17017d38eabe7ae28f18ef0f16d48966cc23a5657e4555fff61704539/zopfli-0.4.1-cp310-abi3-win32.whl", hash = "sha256:a899eca405662a23ae75054affa3517a060362eae1185d3d791c86a50153c4dd", size = 82314, upload-time = "2026-02-13T14:17:20.795Z" }, { url = "https://files.pythonhosted.org/packages/0f/94/806bc84b389c7d70051d7c9a0179cff52de8b9f8dc2fc25bcf0bca302986/zopfli-0.4.1-cp310-abi3-win_amd64.whl", hash = "sha256:84a31ba9edc921b1d3a4449929394a993888f32d70de3a3617800c428a947b9b", size = 102186, upload-time = "2026-02-13T14:17:21.622Z" },]