simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit cf8c763c3031b6123d3a05491ec4be880d8a1b8b
parent c0a11ab37b52e3bf0a8f1e5981500e09abb3c232
Author: Silas Brack <silasbrack@gmail.com>
Date:   Mon, 20 Apr 2026 21:46:49 +0200

refactor: migrate from Python/Starlette to Rust/Axum

- Replace Python (Starlette, Jinja2, aiosqlite) with Rust (Axum, Askama, sqlx)
- Convert Jinja2 templates to Askama syntax
- Port all database queries to sqlx
- Add configurable PORT environment variable
- Update flake.nix for Rust toolchain with crane

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
M.env.example | 9+--------
M.gitignore | 22++++++++--------------
D.python-version | 1-
ACargo.lock | 2359+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACargo.toml | 19+++++++++++++++++++
MREADME.md | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mflake.lock | 89++++++++++++++++++++++++-------------------------------------------------------
Mflake.nix | 355++++++++++++++++++++++++++++---------------------------------------------------
Dlogging.json | 59-----------------------------------------------------------
Dlogging.local.json | 35-----------------------------------
Rsrc/simple_web_app/migrations/20251001193400_add_news_table.sql -> migrations/20251001193400_add_news_table.sql | 0
Dpyproject.toml | 56--------------------------------------------------------
Asrc/config.rs | 27+++++++++++++++++++++++++++
Asrc/db/mod.rs | 5+++++
Asrc/db/models.rs | 48++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/db/queries.rs | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/error.rs | 33+++++++++++++++++++++++++++++++++
Asrc/extractors.rs | 22++++++++++++++++++++++
Asrc/handlers/home.rs | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/handlers/mod.rs | 7+++++++
Asrc/handlers/search.rs | 32++++++++++++++++++++++++++++++++
Asrc/handlers/settings.rs | 28++++++++++++++++++++++++++++
Asrc/main.rs | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/routes.rs | 19+++++++++++++++++++
Dsrc/simple_web_app/__init__.py | 1-
Dsrc/simple_web_app/__main__.py | 11-----------
Dsrc/simple_web_app/app.py | 238-------------------------------------------------------------------------------
Dsrc/simple_web_app/log.py | 59-----------------------------------------------------------
Dsrc/simple_web_app/migration.py | 63---------------------------------------------------------------
Dsrc/simple_web_app/queries/basic.sql | 39---------------------------------------
Dsrc/simple_web_app/templates/application.html | 21---------------------
Dsrc/simple_web_app/templates/chat.html | 0
Dsrc/simple_web_app/templates/index.html | 176-------------------------------------------------------------------------------
Dsrc/simple_web_app/templates/load_more.html | 24------------------------
Dsrc/simple_web_app/templates/news_card.html | 30------------------------------
Dsrc/simple_web_app/templates/news_cards.html | 1-
Dsrc/simple_web_app/templates/oob_swap.html | 1-
Dsrc/simple_web_app/templates/settings.html | 96-------------------------------------------------------------------------------
Dsrc/simple_web_app/templates/settings_tab.html | 501-------------------------------------------------------------------------------
Asrc/state.rs | 39+++++++++++++++++++++++++++++++++++++++
Asrc/templates.rs | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rsrc/simple_web_app/static/style.css -> static/style.css | 0
Atemplates/application.html | 21+++++++++++++++++++++
Atemplates/index.html | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atemplates/load_more.html | 12++++++++++++
Atemplates/news_card.html | 30++++++++++++++++++++++++++++++
Atemplates/news_cards.html | 1+
Atemplates/oob_swap.html | 1+
Rsrc/simple_web_app/templates/search.html -> templates/search.html | 0
Rsrc/simple_web_app/templates/search_results.html -> templates/search_results.html | 0
Atemplates/settings.html | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atemplates/settings_tab.html | 503+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtests/locustfile.py | 7-------
Dtests/test_simple_web_app/conftest.py | 729-------------------------------------------------------------------------------
Dtests/test_simple_web_app/test_app.py | 170-------------------------------------------------------------------------------
Duv.lock | 1043-------------------------------------------------------------------------------
56 files changed, 4098 insertions(+), 3688 deletions(-)

diff --git a/.env.example b/.env.example @@ -1,10 +1,3 @@ -# Uvicorn settings -UVICORN_RELOAD=true -UVICORN_RELOAD_DIRS=src -UVICORN_RELOAD_INCLUDES=*.html *.css *.json *.sql -UVICORN_LOG_CONFIG=logging.local.json - # Application settings DEBUG=true -DATABASE_PATH=db.sqlite3 - +DATABASE_URL=sqlite:./db.sqlite3 diff --git a/.gitignore b/.gitignore @@ -1,17 +1,5 @@ -# Python-generated files -__pycache__/ -*.py[oc] -build/ -dist/ -wheels/ -*.egg-info/ - -# Virtual environments -.venv/ - -# Caches -.ruff_cache/ -.pytest_cache/ +# Rust +/target/ # Sqlite *.sqlite3* @@ -25,3 +13,9 @@ result # Environment .env +# IDE +*.swp +tags +TAGS +.idea/ +*.iml diff --git a/.python-version b/.python-version @@ -1 +0,0 @@ -3.13 diff --git a/Cargo.lock b/Cargo.lock @@ -0,0 +1,2359 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[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 = "askama" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" +dependencies = [ + "askama_derive", + "askama_escape", + "humansize", + "num-traits", + "percent-encoding", +] + +[[package]] +name = "askama_axum" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41603f7cdbf5ac4af60760f17253eb6adf6ec5b6f14a7ed830cf687d375f163" +dependencies = [ + "askama", + "axum-core 0.4.5", + "http", +] + +[[package]] +name = "askama_derive" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" +dependencies = [ + "askama_parser", + "basic-toml", + "mime", + "mime_guess", + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "askama_escape" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" + +[[package]] +name = "askama_parser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" +dependencies = [ + "nom", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[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 0.5.6", + "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.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[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 = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +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", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[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 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[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" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[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 = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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 = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[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 = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[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", +] + +[[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.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.4", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[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 = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[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 = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[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 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[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 = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[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 = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[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", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" +dependencies = [ + "bitflags", +] + +[[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 = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[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 = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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 = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[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 = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simple-web-app" +version = "0.1.0" +dependencies = [ + "askama", + "askama_axum", + "axum", + "chrono", + "dotenvy", + "futures", + "serde", + "sqlx", + "thiserror", + "tokio", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[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 = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[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-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +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", + "tracing", +] + +[[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-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[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-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[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.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[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 = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[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 = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "simple-web-app" +version = "0.1.0" +edition = "2021" + +[dependencies] +axum = "0.8" +askama = { version = "0.12", features = ["with-axum"] } +askama_axum = "0.4" +sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono"] } +tokio = { version = "1", features = ["full"] } +tower-http = { version = "0.6", features = ["fs", "trace"] } +chrono = { version = "0.4", features = ["serde"] } +serde = { version = "1", features = ["derive"] } +dotenvy = "0.15" +thiserror = "2" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +futures = "0.3" diff --git a/README.md b/README.md @@ -1,22 +1,93 @@ # Simple Web App -## Development Guidelines +A news aggregation web application built with Rust, Axum, Askama, and SQLite. + +## Tech Stack -We use `uv2nix` to build a python package and a virtual environment with all its dependencies. -Everything in the `src/` folder should get included in the built Python package. -The entire web app logic should be contained within the Python package and consequently in the `src/` folder. +- **Axum** - Web framework +- **Askama** - Type-safe HTML templating +- **SQLx** - Async SQL toolkit with compile-time checked queries +- **HTMX** - Frontend interactivity without JavaScript frameworks +- **PicoCSS** - Minimal CSS framework -Configuration, on the other hand, should, whenever possible, be kept outside of the Python package. -Keep the application itself configuration-agnostic whenever possible and choose to instead maintain the `.env.example` file containing a suggested default configuration for developers. -Configuration should thus always be performed via environment variables. -In this way and some others, we follow the principles of the [12-factor app](https://12factor.net/). +## Development Guidelines + +Configuration should be performed via environment variables following the [12-factor app](https://12factor.net/) principles. ## Developer Setup -Since configuration is all performed via environment variables and everything else is stored in the package, all you need to do to get started is: -1. Run `cp .env.example .env`: Copy the example environment file. Since we're not really using any secrets, the default configuration should suffice for now. -2. Load environment variables from the `.env` file. I have a function `loadenv` which does this for me. Lots of people do this automatically with [direnv](https://direnv.net/), and VS Code's Python extension does this automatically too. -3. Run `uv sync` or `nix develop .#impure` followed by `uv sync` or `nix develop .#uv2nix`: Create a virtual environment and install the required dependencies. -4. Run either `uv run simple-web-app` (if you used `uv sync` above) or `simple-web-app` (if you used the `.#uv2nix` approach): Run the server locally. The application should automatically create a database file (based on the value of `DATABASE_PATH` from your `.env` file) and apply the necessary migrations to it. By default, `uvicorn` will reload the web server whenever files in the `src/` folder change. -5. Go to [localhost:8000](http://localhost:8000/) (assuming you didn't override the `UVICORN_PORT` variable in the `.env` file) to see the page. +### With Nix (recommended) + +1. Copy the example environment file: + ```bash + cp .env.example .env + ``` + +2. Enter the development shell: + ```bash + nix develop + ``` + +3. Run the server: + ```bash + cargo run + ``` + +4. For auto-reload during development: + ```bash + cargo watch -x run + ``` + +### Without Nix + +1. Install Rust via [rustup](https://rustup.rs/) + +2. Copy the example environment file: + ```bash + cp .env.example .env + ``` + +3. Run the server: + ```bash + cargo run + ``` + +4. Go to [localhost:8000](http://localhost:8000/) to see the page. + +## Building for Production + +```bash +nix build +``` + +Or without Nix: + +```bash +cargo build --release +``` + +## Project Structure +``` +simple-web-app/ +├── Cargo.toml # Rust dependencies +├── flake.nix # Nix flake configuration +├── migrations/ # SQLite migrations +├── static/ # Static assets (CSS) +├── templates/ # Askama HTML templates +└── src/ + ├── main.rs # Entry point + ├── config.rs # Environment configuration + ├── state.rs # Application state (DB pool) + ├── error.rs # Error types + ├── routes.rs # Route definitions + ├── extractors.rs # Custom Axum extractors + ├── templates.rs # Askama template structs + ├── handlers/ # Route handlers + │ ├── home.rs + │ ├── search.rs + │ └── settings.rs + └── db/ # Database layer + ├── models.rs + └── queries.rs +``` diff --git a/flake.lock b/flake.lock @@ -1,95 +1,60 @@ { "nodes": { - "nixpkgs": { + "crane": { "locked": { - "lastModified": 1757347588, - "narHash": "sha256-tLdkkC6XnsY9EOZW9TlpesTclELy8W7lL2ClL+nma8o=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b599843bad24621dcaa5ab60dac98f9b0eb1cabe", + "lastModified": 1776635034, + "narHash": "sha256-OEOJrT3ZfwbChzODfIH4GzlNTtOFuZFWPtW7jIeR8xU=", + "owner": "ipetkov", + "repo": "crane", + "rev": "dc7496d8ea6e526b1254b55d09b966e94673750f", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", + "owner": "ipetkov", + "repo": "crane", "type": "github" } }, - "pyproject-build-systems": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ], - "pyproject-nix": [ - "pyproject-nix" - ], - "uv2nix": [ - "uv2nix" - ] - }, - "locked": { - "lastModified": 1757296493, - "narHash": "sha256-6nzSZl28IwH2Vx8YSmd3t6TREHpDbKlDPK+dq1LKIZQ=", - "owner": "pyproject-nix", - "repo": "build-system-pkgs", - "rev": "5b8e37fe0077db5c1df3a5ee90a651345f085d38", - "type": "github" - }, - "original": { - "owner": "pyproject-nix", - "repo": "build-system-pkgs", - "type": "github" - } - }, - "pyproject-nix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, + "nixpkgs": { "locked": { - "lastModified": 1757246327, - "narHash": "sha256-6pNlGhwOIMfhe/RLjHdpXveKS4FyLHvlGe+KtjDild4=", - "owner": "pyproject-nix", - "repo": "pyproject.nix", - "rev": "8d77f342d66ad1601cdb9d97e9388b69f64d4c8e", + "lastModified": 1776548001, + "narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc", "type": "github" }, "original": { - "owner": "pyproject-nix", - "repo": "pyproject.nix", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { + "crane": "crane", "nixpkgs": "nixpkgs", - "pyproject-build-systems": "pyproject-build-systems", - "pyproject-nix": "pyproject-nix", - "uv2nix": "uv2nix" + "rust-overlay": "rust-overlay" } }, - "uv2nix": { + "rust-overlay": { "inputs": { "nixpkgs": [ "nixpkgs" - ], - "pyproject-nix": [ - "pyproject-nix" ] }, "locked": { - "lastModified": 1756973152, - "narHash": "sha256-9JcKAA7T9J98LWdcxbXvmf+amQG3ZErxqQnBjEJI04I=", - "owner": "pyproject-nix", - "repo": "uv2nix", - "rev": "64298e806f4a5f63a51c625edc100348138491aa", + "lastModified": 1776654897, + "narHash": "sha256-Vqi4AiJVCcBGn/RmBtRCgyH5rCxqm/w0xV9diJWF1Ic=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "25d75be8139815a53560745fa060909777495105", "type": "github" }, "original": { - "owner": "pyproject-nix", - "repo": "uv2nix", + "owner": "oxalica", + "repo": "rust-overlay", "type": "github" } } diff --git a/flake.nix b/flake.nix @@ -1,288 +1,185 @@ { - description = "A Nix-flake-based Python development environment"; + description = "A Nix-flake-based Rust web application"; + inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - - pyproject-nix = { - url = "github:pyproject-nix/pyproject.nix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - - uv2nix = { - url = "github:pyproject-nix/uv2nix"; - inputs.pyproject-nix.follows = "pyproject-nix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - - pyproject-build-systems = { - url = "github:pyproject-nix/build-system-pkgs"; - inputs.pyproject-nix.follows = "pyproject-nix"; - inputs.uv2nix.follows = "uv2nix"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; + crane.url = "github:ipetkov/crane"; }; + outputs = { self, nixpkgs, - uv2nix, - pyproject-nix, - pyproject-build-systems, + rust-overlay, + crane, ... }: let inherit (nixpkgs) lib; forAllSystems = lib.genAttrs lib.systems.flakeExposed; - # Load a uv workspace from a workspace root. - # Uv2nix treats all uv projects as workspace projects. - workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; }; - - # Create package overlay from workspace. - overlay = workspace.mkPyprojectOverlay { - sourcePreference = "wheel"; - }; - - # Construct package set - pythonSets = forAllSystems ( + mkPkgs = + system: + import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) ]; + }; + in + { + packages = forAllSystems ( system: let - pkgs = nixpkgs.legacyPackages.${system}; - inherit (pkgs) stdenv; - baseSet = pkgs.callPackage pyproject-nix.build.packages { - python = pkgs.python313; - }; + pkgs = mkPkgs system; + rustToolchain = pkgs.rust-bin.stable.latest.default; + craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; + + # Common arguments for crane builds + commonArgs = { + src = ./.; + pname = "simple-web-app"; + version = "0.1.0"; + strictDeps = true; + + buildInputs = with pkgs; [ + openssl + ] ++ lib.optionals pkgs.stdenv.isDarwin [ + pkgs.darwin.apple_sdk.frameworks.Security + pkgs.darwin.apple_sdk.frameworks.SystemConfiguration + ]; - includeLoggingConfigOverride = _final: prev: { - simple-web-app = prev.simple-web-app.overrideAttrs (old: { - postInstall = '' - mkdir -p $out/share - cp ${./logging.json} $out/share/logging.json - ''; - }); + nativeBuildInputs = with pkgs; [ + pkg-config + ]; }; - # Extend generated overlay with build fixups - # - # Uv2nix can only work with what it has, and uv.lock is missing essential metadata to perform some builds. - # This is an additional overlay implementing build fixups. - # See: - # - https://pyproject-nix.github.io/uv2nix/FAQ.html - pyprojectOverrides = final: prev: { - # Implement build fixups here. - # Note that uv2nix is _not_ using Nixpkgs buildPythonPackage. - # It's using https://pyproject-nix.github.io/pyproject.nix/build.html - simple-web-app = prev.simple-web-app.overrideAttrs (old: { - passthru = old.passthru // { - # Put all tests in the passthru.tests attribute set. - # Nixpkgs also uses the passthru.tests mechanism for ofborg test discovery. - # - # For usage with Flakes we will refer to the passthru.tests attributes to construct the flake checks attribute set. - tests = - let - # Construct a virtual environment with only the test dependency-group enabled. - virtualenv = final.mkVirtualEnv "simple-web-app-pytest-env" { - simple-web-app = [ "test" ]; - }; - - in - (old.tests or { }) - // { - pytest = stdenv.mkDerivation { - name = "${final.simple-web-app.name}-pytest"; - inherit (final.simple-web-app) src; - nativeBuildInputs = [ - virtualenv - ]; - dontConfigure = true; - - # Because this package is running tests, and not actually building the main package - # the build phase is running the tests. - # - # In this particular example we also output a HTML coverage report, which is used as the build output. - buildPhase = '' - runHook preBuild - pytest --cov tests --cov-report html - runHook postBuild - ''; + # Build just the cargo dependencies + cargoArtifacts = craneLib.buildDepsOnly commonArgs; - # Install the HTML coverage report into the build output. - # - # If you wanted to install multiple test output formats such as TAP outputs - # you could make this derivation a multiple-output derivation. - # - # See https://nixos.org/manual/nixpkgs/stable/#chap-multiple-output for more information on multiple outputs. - installPhase = '' - runHook preInstall - mv htmlcov $out - runHook postInstall - ''; - }; - - }; - }; - }); - }; + # Build the actual crate + simple-web-app = craneLib.buildPackage ( + commonArgs + // { + inherit cargoArtifacts; + } + ); in - # Use base package set from pyproject.nix builders - baseSet.overrideScope ( - lib.composeManyExtensions [ - pyproject-build-systems.overlays.default - overlay - pyprojectOverrides - includeLoggingConfigOverride - ] - ) + { + default = simple-web-app; + inherit simple-web-app; + } ); - # inherit (pkgs.callPackages pyproject-nix.build.util { }) mkApplication; - in - { - # Package a virtual environment as our main application. - # - # Enable no optional dependencies for production build. - packages = forAllSystems (system: { - default = pythonSets.${system}.mkVirtualEnv "simple-web-app-env" workspace.deps.default; - }); - - # default = mkApplication { - # venv = pythonSet.mkVirtualEnv "simple-web-app-env" workspace.deps.default; - # package = pythonSet.hello-world; - # }; - # Make simple-web-app runnable with `nix run` apps = forAllSystems ( system: let - appRoot = self.packages.${system}.default; - appProgram = "${appRoot}/bin/simple-web-app"; + pkgs = mkPkgs system; in { default = { type = "app"; - program = appProgram; + program = "${self.packages.${system}.default}/bin/simple-web-app"; }; } ); - # Run the checks by running `nix flake check` + # Run checks with `nix flake check` checks = forAllSystems ( system: let - pythonSet = pythonSets.${system}; + pkgs = mkPkgs system; + rustToolchain = pkgs.rust-bin.stable.latest.default; + craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; + + commonArgs = { + src = ./.; + pname = "simple-web-app"; + version = "0.1.0"; + strictDeps = true; + + buildInputs = with pkgs; [ + openssl + ] ++ lib.optionals pkgs.stdenv.isDarwin [ + pkgs.darwin.apple_sdk.frameworks.Security + pkgs.darwin.apple_sdk.frameworks.SystemConfiguration + ]; + + nativeBuildInputs = with pkgs; [ + pkg-config + ]; + }; + + cargoArtifacts = craneLib.buildDepsOnly commonArgs; in { - inherit (pythonSet.simple-web-app.passthru.tests) pytest; + # Build the crate as part of checks + simple-web-app = self.packages.${system}.default; + + # Run clippy + clippy = craneLib.cargoClippy ( + commonArgs + // { + inherit cargoArtifacts; + cargoClippyExtraArgs = "--all-targets -- --deny warnings"; + } + ); + + # Check formatting + fmt = craneLib.cargoFmt { src = ./.; }; + + # Run tests + test = craneLib.cargoTest ( + commonArgs + // { + inherit cargoArtifacts; + } + ); } ); - # This example provides two different modes of development: - # - Impurely using uv to manage virtual environments - # - Pure development using uv2nix to manage virtual environments devShells = forAllSystems ( system: let - pkgs = nixpkgs.legacyPackages.${system}; - python = pkgs.python313; + pkgs = mkPkgs system; + rustToolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ + "rust-src" + "rust-analyzer" + ]; + }; in { - # It is of course perfectly OK to keep using an impure virtualenv workflow and only use uv2nix to build packages. - # This devShell simply adds Python and undoes the dependency leakage done by Nixpkgs Python infrastructure. - impure = pkgs.mkShell { - packages = [ - python - pkgs.uv - pkgs.ruff - pkgs.ty + default = pkgs.mkShell { + packages = with pkgs; [ + rustToolchain + pkg-config + openssl + sqlite + + # Development tools + cargo-watch + cargo-edit + rust-analyzer + ] ++ lib.optionals pkgs.stdenv.isDarwin [ + pkgs.darwin.apple_sdk.frameworks.Security + pkgs.darwin.apple_sdk.frameworks.SystemConfiguration ]; - env = { - # Prevent uv from managing Python downloads - UV_PYTHON_DOWNLOADS = "never"; - # Force uv to use nixpkgs Python interpreter - UV_PYTHON = python.interpreter; - - LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib/:/run/opengl-driver/lib/"; + env = { + RUST_BACKTRACE = "1"; + RUST_LOG = "debug"; }; + shellHook = '' - unset PYTHONPATH + echo "Rust development environment loaded" + echo "Run 'cargo run' to start the server" + echo "Run 'cargo watch -x run' for auto-reload" ''; }; - - # This devShell uses uv2nix to construct a virtual environment purely from Nix, using the same dependency specification as the application. - # The notable difference is that we also apply another overlay here enabling editable mode ( https://setuptools.pypa.io/en/latest/userguide/development_mode.html ). - # - # This means that any changes done to your local files do not require a rebuild. - # - # Note: Editable package support is still unstable and subject to change. - uv2nix = - let - # Create an overlay enabling editable mode for all local dependencies. - editableOverlay = workspace.mkEditablePyprojectOverlay { - # Use environment variable - root = "$REPO_ROOT"; - # Optional: Only enable editable for these packages - # members = [ "hello-world" ]; - }; - - pythonSet = pythonSets.${system}.overrideScope editableOverlay; - - # Override previous set with our overrideable overlay. - editablePythonSet = pythonSet.overrideScope ( - lib.composeManyExtensions [ - editableOverlay - - # Apply fixups for building an editable package of your workspace packages - (final: prev: { - simple-web-app = prev.simple-web-app.overrideAttrs (old: { - # It's a good idea to filter the sources going into an editable build - # so the editable package doesn't have to be rebuilt on every change. - src = lib.fileset.toSource { - root = old.src; - fileset = lib.fileset.unions [ - (old.src + "/pyproject.toml") - (old.src + "/README.md") - (old.src + "/src/simple_web_app/__init__.py") - ]; - }; - }); - }) - ] - ); - - # Build virtual environment, with local packages being editable. - # - # Enable all optional dependencies for development. - virtualenv = editablePythonSet.mkVirtualEnv "simple-web-app-dev-env" workspace.deps.all; - - in - pkgs.mkShell { - packages = [ - virtualenv - pkgs.uv - pkgs.ruff - pkgs.ty - ]; - - env = { - # Don't create venv using uv - UV_NO_SYNC = "1"; - - # Force uv to use nixpkgs Python interpreter - UV_PYTHON = python.interpreter; - - # Prevent uv from downloading managed Python's - UV_PYTHON_DOWNLOADS = "never"; - }; - - shellHook = '' - # Undo dependency propagation by nixpkgs. - unset PYTHONPATH - - # Get repository root using git. This is expanded at runtime by the editable `.pth` machinery. - export REPO_ROOT=$(git rev-parse --show-toplevel) - ''; - }; } ); }; diff --git a/logging.json b/logging.json @@ -1,59 +0,0 @@ -{ - "version": 1, - "formatters": { - "default": { - "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - }, - "extra": { - "format": "%(asctime)-16s %(name)-8s %(filename)-12s %(lineno)-6s %(funcName)-30s %(levelname)-8s %(message)s", - "datefmt": "%m-%d %H:%M:%S" - }, - "json": { - "()": "simple_web_app.log.JsonFormatter", - "fmt_dict": { - "level": "levelname", - "message": "message", - "loggerName": "name", - "processName": "processName", - "processID": "process", - "threadName": "threadName", - "threadID": "thread", - "timestamp": "asctime" - } - } - }, - "handlers": { - "root": { - "class": "logging.StreamHandler", - "level": "WARN", - "formatter": "json", - "stream": "ext://sys.stdout" - }, - "simple_web_app": { - "class": "logging.StreamHandler", - "level": "INFO", - "formatter": "json", - "stream": "ext://sys.stdout" - }, - "uvicorn": { - "class": "logging.StreamHandler", - "level": "INFO", - "formatter": "json", - "stream": "ext://sys.stdout" - } - }, - "loggers": { - "simple_web_app": { - "level": "INFO", - "handlers": ["simple_web_app"] - }, - "uvicorn": { - "level": "INFO", - "handlers": ["uvicorn"] - } - }, - "root": { - "level": "WARN", - "handlers": ["root"] - } -} diff --git a/logging.local.json b/logging.local.json @@ -1,35 +0,0 @@ -{ - "version": 1, - "formatters": { - "default": { - "format": "%(asctime)s - %(levelname)-5s - %(name)-20.20s - %(filename)s:%(lineno)s - %(funcName)s - %(message)s", - "datefmt": "%m-%d %H:%M:%S" - } - }, - "handlers": { - "default": { - "class": "logging.StreamHandler", - "level": "DEBUG", - "formatter": "default", - "stream": "ext://sys.stdout" - } - }, - "loggers": { - "simple_web_app": { - "level": "INFO", - "handlers": ["default"] - }, - "uvicorn": { - "level": "INFO", - "handlers": ["default"] - }, - "aiosqlite": { - "level": "DEBUG", - "handlers": ["default"] - } - }, - "root": { - "level": "WARN", - "handlers": ["default"] - } -} diff --git a/src/simple_web_app/migrations/20251001193400_add_news_table.sql b/migrations/20251001193400_add_news_table.sql diff --git a/pyproject.toml b/pyproject.toml @@ -1,56 +0,0 @@ -[project] -name = "simple-web-app" -version = "0.1.0" -description = "Add your description here" -readme = "README.md" -authors = [ - { name = "Silas Brack", email = "silasbrack@gmail.com" } -] -requires-python = "==3.13.*" -dependencies = [ - # HTTP server - "uvicorn", - "uvloop", - "httptools", - # Web framework - "starlette", - "jinja2", - "python-multipart", - "aiosql", # Loading SQL queries as Python functions - "aiosqlite", # Async sqlite driver - "argon2-cffi", # Password hashing - "orjson", # Serializing logs as JSON -] - -[dependency-groups] -dev = [ - { include-group = "test" }, - "ruff", - "watchfiles", # Watch non-*.py files with uvicorn -] -test = [ - "pytest", - "pytest-cov", - "pytest-asyncio", - "httpx", # Starlette TestClient support - "locust", # Stress testing - "beautifulsoup4", # HTML parsing -] - -[project.scripts] -simple-web-app = "simple_web_app.__main__:run_uvicorn" -create-migration = "simple_web_app.migration:run_create_migration" - -[build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" - -[tool.setuptools.packages.find] -where = ["src"] - -[tool.setuptools.package-data] -simple_web_app = ["queries/**", "migrations/**", "templates/**", "static/**"] - -[tool.pytest.ini_options] -asyncio_mode = "auto" - diff --git a/src/config.rs b/src/config.rs @@ -0,0 +1,27 @@ +use std::env; + +pub struct Config { + pub database_url: String, + pub debug: bool, + pub port: u16, +} + +impl Config { + pub fn from_env() -> Self { + dotenvy::dotenv().ok(); + + let database_url = + env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite:./db.sqlite3".to_string()); + + let debug = env::var("DEBUG") + .map(|v| v.to_lowercase() == "true" || v == "1") + .unwrap_or(true); + + let port = env::var("PORT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(8000); + + Self { database_url, debug, port } + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs @@ -0,0 +1,5 @@ +pub mod models; +pub mod queries; + +pub use models::*; +pub use queries::*; diff --git a/src/db/models.rs b/src/db/models.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] +pub struct NewsItem { + pub id: i64, + pub title: String, + pub text: String, + pub published: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] +pub struct SearchResult { + pub title: String, + pub text: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] +pub struct Category { + pub id: i64, + pub name: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NewsWithCategories { + pub id: i64, + pub title: String, + pub text: String, + pub published: String, + pub categories: Vec<Category>, + pub time_since_published: String, +} + +impl NewsWithCategories { + pub fn from_news_item( + item: NewsItem, + categories: Vec<Category>, + time_since_published: String, + ) -> Self { + Self { + id: item.id, + title: item.title, + text: item.text, + published: item.published, + categories, + time_since_published, + } + } +} diff --git a/src/db/queries.rs b/src/db/queries.rs @@ -0,0 +1,106 @@ +use sqlx::SqlitePool; + +use super::models::{Category, NewsItem, SearchResult}; + +pub async fn get_news( + pool: &SqlitePool, + limit: i64, + offset: i64, + max_published_time: &str, +) -> Result<Vec<NewsItem>, sqlx::Error> { + sqlx::query_as::<_, NewsItem>( + r#" + SELECT id, title, text, published + FROM news_item + WHERE language = 'english' + AND (published < ?) + ORDER BY published DESC, id ASC + LIMIT ? + OFFSET ? + "#, + ) + .bind(max_published_time) + .bind(limit) + .bind(offset) + .fetch_all(pool) + .await +} + +pub async fn get_news_by_category( + pool: &SqlitePool, + category_id: i64, + limit: i64, + offset: i64, + max_published_time: &str, +) -> Result<Vec<NewsItem>, sqlx::Error> { + sqlx::query_as::<_, NewsItem>( + r#" + SELECT ni.id, ni.title, ni.text, ni.published + FROM news_item AS ni + INNER JOIN news_item_category AS nic + ON nic.news_item_id = ni.id + WHERE ni.language = 'english' + AND (ni.published < ?) + AND nic.category_id = ? + ORDER BY ni.published DESC, ni.id ASC + LIMIT ? + OFFSET ? + "#, + ) + .bind(max_published_time) + .bind(category_id) + .bind(limit) + .bind(offset) + .fetch_all(pool) + .await +} + +pub async fn search_news( + pool: &SqlitePool, + query: &str, + limit: i64, +) -> Result<Vec<SearchResult>, sqlx::Error> { + sqlx::query_as::<_, SearchResult>( + r#" + SELECT title, text + FROM news_item_fts(?) + LIMIT ? + "#, + ) + .bind(query) + .bind(limit) + .fetch_all(pool) + .await +} + +pub async fn get_categories_for_news( + pool: &SqlitePool, + news_item_id: i64, +) -> Result<Vec<Category>, sqlx::Error> { + sqlx::query_as::<_, Category>( + r#" + SELECT c.id, c.name + FROM news_item_category AS nic + INNER JOIN category AS c + ON c.id = nic.category_id + WHERE nic.news_item_id = ? + "#, + ) + .bind(news_item_id) + .fetch_all(pool) + .await +} + +pub async fn get_categories(pool: &SqlitePool, limit: i64) -> Result<Vec<Category>, sqlx::Error> { + sqlx::query_as::<_, Category>( + r#" + SELECT id, name + FROM category + ORDER BY id + LIMIT ? + "#, + ) + .bind(limit) + .fetch_all(pool) + .await +} diff --git a/src/error.rs b/src/error.rs @@ -0,0 +1,33 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AppError { + #[error("Database error: {0}")] + Database(#[from] sqlx::Error), + + #[error("Template error: {0}")] + Template(#[from] askama::Error), + + #[error("Not found")] + NotFound, +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let (status, message) = match &self { + AppError::Database(e) => { + tracing::error!("Database error: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "Database error") + } + AppError::Template(e) => { + tracing::error!("Template error: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "Template error") + } + AppError::NotFound => (StatusCode::NOT_FOUND, "Not found"), + }; + + (status, message).into_response() + } +} diff --git a/src/extractors.rs b/src/extractors.rs @@ -0,0 +1,22 @@ +use axum::{ + extract::FromRequestParts, + http::{request::Parts, StatusCode}, +}; + +/// Extractor that checks if the request is an HTMX request. +/// Returns true if HX-Request header is present OR HX-History-Restore-Request is present. +pub struct HtmxRequest(pub bool); + +impl<S> FromRequestParts<S> for HtmxRequest +where + S: Send + Sync, +{ + type Rejection = (StatusCode, &'static str); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { + let is_hx_request = parts.headers.get("HX-Request").is_some(); + let is_hx_history_restore = parts.headers.get("HX-History-Restore-Request").is_some(); + + Ok(HtmxRequest(is_hx_request || is_hx_history_restore)) + } +} diff --git a/src/handlers/home.rs b/src/handlers/home.rs @@ -0,0 +1,112 @@ +use askama::Template; +use axum::{ + extract::{Query, State}, + response::Html, +}; +use chrono::{DateTime, Utc}; +use serde::Deserialize; + +use crate::db::{self, NewsWithCategories}; +use crate::error::AppError; +use crate::extractors::HtmxRequest; +use crate::state::AppState; +use crate::templates::{ApplicationTemplate, IndexTemplate, OobSwapTemplate}; + +#[derive(Debug, Deserialize)] +pub struct HomeQuery { + #[serde(default)] + pub page: i64, + pub current_time: Option<String>, + pub category_id: Option<i64>, +} + +fn format_time_since_published(published: &str, current_time: DateTime<Utc>) -> String { + // Parse the published timestamp + let published_time = DateTime::parse_from_str(published, "%Y-%m-%dT%H:%M:%S%.f%z") + .or_else(|_| DateTime::parse_from_rfc3339(published)) + .map(|dt| dt.with_timezone(&Utc)) + .unwrap_or(current_time); + + let duration = current_time.signed_duration_since(published_time); + + let days = duration.num_days(); + let hours = duration.num_hours() % 24; + + format!("{} days, {} hours ago", days, hours) +} + +pub async fn show_home_page( + State(state): State<AppState>, + HtmxRequest(is_htmx): HtmxRequest, + Query(query): Query<HomeQuery>, +) -> Result<Html<String>, AppError> { + let page = query.page; + let current_time_str = query + .current_time + .unwrap_or_else(|| Utc::now().to_rfc3339()); + let category_id = query.category_id; + + let limit: i64 = 5; + let offset = page * limit; + + // Fetch categories and news concurrently + let (all_categories, rows) = tokio::try_join!( + db::get_categories(&state.pool, 20), + async { + if let Some(cat_id) = category_id { + db::get_news_by_category(&state.pool, cat_id, limit + 1, offset, &current_time_str) + .await + } else { + db::get_news(&state.pool, limit + 1, offset, &current_time_str).await + } + } + )?; + + let reached_end = rows.len() as i64 <= limit; + let rows: Vec<_> = rows.into_iter().take(limit as usize).collect(); + + // Fetch categories for each news item concurrently + let categories_futures: Vec<_> = rows + .iter() + .map(|row| db::get_categories_for_news(&state.pool, row.id)) + .collect(); + + let categories_results = futures::future::try_join_all(categories_futures).await?; + + let current_time_utc = Utc::now(); + + let news: Vec<NewsWithCategories> = rows + .into_iter() + .zip(categories_results) + .map(|(row, cats)| { + let time_since = format_time_since_published(&row.published, current_time_utc); + NewsWithCategories::from_news_item(row, cats, time_since) + }) + .collect(); + + if is_htmx { + let template = OobSwapTemplate { + news, + page, + current_time: current_time_str, + category_id, + reached_end, + oob: true, + }; + Ok(Html(template.render()?)) + } else { + let index_template = IndexTemplate { + news, + categories: all_categories, + page, + current_time: current_time_str, + category_id, + reached_end, + oob: false, + }; + let content = index_template.render()?; + + let app_template = ApplicationTemplate { content }; + Ok(Html(app_template.render()?)) + } +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs @@ -0,0 +1,7 @@ +pub mod home; +pub mod search; +pub mod settings; + +pub use home::*; +pub use search::*; +pub use settings::*; diff --git a/src/handlers/search.rs b/src/handlers/search.rs @@ -0,0 +1,32 @@ +use askama::Template; +use axum::{extract::State, response::Html, Form}; +use serde::Deserialize; + +use crate::db; +use crate::error::AppError; +use crate::state::AppState; +use crate::templates::{SearchResultsTemplate, SearchTemplate}; + +pub async fn open_search() -> Result<Html<String>, AppError> { + let template = SearchTemplate; + Ok(Html(template.render()?)) +} + +#[derive(Debug, Deserialize)] +pub struct SearchForm { + pub search: String, +} + +pub async fn search( + State(state): State<AppState>, + Form(form): Form<SearchForm>, +) -> Result<Html<String>, AppError> { + let news = if form.search.is_empty() { + Vec::new() + } else { + db::search_news(&state.pool, &form.search, 10).await? + }; + + let template = SearchResultsTemplate { news }; + Ok(Html(template.render()?)) +} diff --git a/src/handlers/settings.rs b/src/handlers/settings.rs @@ -0,0 +1,28 @@ +use askama::Template; +use axum::{extract::Query, response::Html}; +use serde::Deserialize; + +use crate::error::AppError; +use crate::templates::{SettingsTabTemplate, SettingsTemplate}; + +#[derive(Debug, Deserialize)] +pub struct SettingsQuery { + #[serde(default = "default_tab")] + pub tab: String, +} + +fn default_tab() -> String { + "general".to_string() +} + +pub async fn open_settings(Query(query): Query<SettingsQuery>) -> Result<Html<String>, AppError> { + let template = SettingsTemplate { tab: query.tab }; + Ok(Html(template.render()?)) +} + +pub async fn open_settings_tab( + Query(query): Query<SettingsQuery>, +) -> Result<Html<String>, AppError> { + let template = SettingsTabTemplate { tab: query.tab }; + Ok(Html(template.render()?)) +} diff --git a/src/main.rs b/src/main.rs @@ -0,0 +1,75 @@ +use axum::{ + body::Body, + http::{Request, Response}, + middleware::{self, Next}, +}; +use tower_http::trace::TraceLayer; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +mod config; +mod db; +mod error; +mod extractors; +mod handlers; +mod routes; +mod state; +mod templates; + +use config::Config; +use state::AppState; + +/// Middleware that sets Cache-Control for preloaded HTMX requests +async fn cache_control_middleware(request: Request<Body>, next: Next) -> Response<Body> { + let is_preloaded = request + .headers() + .get("HX-Preloaded") + .map(|v| v == "true") + .unwrap_or(false); + + let mut response = next.run(request).await; + + if is_preloaded { + response.headers_mut().insert( + "Cache-Control", + "private, max-age=60".parse().unwrap(), + ); + } + + response +} + +#[tokio::main] +async fn main() -> Result<(), Box<dyn std::error::Error>> { + // Initialize tracing + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "simple_web_app=debug,tower_http=debug".into()), + ) + .init(); + + // Load configuration + let config = Config::from_env(); + + tracing::info!("Starting server with DEBUG={}", config.debug); + + // Create application state + let state = AppState::new(&config).await?; + + tracing::info!("Database connected and migrations applied"); + + // Build router with middleware + let app = routes::create_router(state) + .layer(middleware::from_fn(cache_control_middleware)) + .layer(TraceLayer::new_for_http()); + + // Serve on configured port + let addr = format!("0.0.0.0:{}", config.port); + let listener = tokio::net::TcpListener::bind(&addr).await?; + tracing::info!("Server listening on http://{}", addr); + + axum::serve(listener, app).await?; + + Ok(()) +} diff --git a/src/routes.rs b/src/routes.rs @@ -0,0 +1,19 @@ +use axum::{ + Router, + routing::{get, post}, +}; +use tower_http::services::ServeDir; + +use crate::handlers; +use crate::state::AppState; + +pub fn create_router(state: AppState) -> Router { + Router::new() + .route("/", get(handlers::show_home_page)) + .route("/search", get(handlers::open_search)) + .route("/search", post(handlers::search)) + .route("/settings", get(handlers::open_settings)) + .route("/settings/tab", get(handlers::open_settings_tab)) + .nest_service("/static", ServeDir::new("static")) + .with_state(state) +} diff --git a/src/simple_web_app/__init__.py b/src/simple_web_app/__init__.py @@ -1 +0,0 @@ - diff --git a/src/simple_web_app/__main__.py b/src/simple_web_app/__main__.py @@ -1,11 +0,0 @@ -import subprocess -import sys - - -def run_uvicorn(): - subprocess.run([sys.executable, "-m", "uvicorn", "simple_web_app.app:app"]) - - -if __name__ == "__main__": - run_uvicorn() - diff --git a/src/simple_web_app/app.py b/src/simple_web_app/app.py @@ -1,238 +0,0 @@ -import asyncio -import contextlib -import importlib.resources -import logging -import datetime -from pathlib import Path - -import aiosql -import aiosqlite -from starlette.applications import Starlette -from starlette.config import Config -from starlette.middleware import Middleware -from starlette.middleware.base import BaseHTTPMiddleware - -# from starlette.middleware.sessions import SessionMiddleware -from starlette.requests import Request -from starlette.routing import Mount, Route -from starlette.staticfiles import StaticFiles -from starlette.templating import Jinja2Templates - -from simple_web_app.migration import ( - apply_migrations, - create_migrations_table_if_not_exists, -) - -logger = logging.getLogger(__name__) - -SQLITE_PRAGMAS = [ - "PRAGMA journal_mode = WAL;", - "PRAGMA foreign_keys = ON;", - "PRAGMA synchronous = NORMAL;", - "PRAGMA cache_size = 1000000000;", - "PRAGMA temp_store = MEMORY;", - "PRAGMA busy_timeout = 5000;", - "PRAGMA foreign_keys = ON;", - "PRAGMA synchronous = NORMAL;", -] - -config = Config() -DEBUG = config("DEBUG", cast=bool, default=True) -DATABASE_PATH = config("DATABASE_PATH", default="./db.sqlite3") -# SECRET_KEY = config("SECRET_KEY", default="asdf") - -DATABASE_PATH = Path(DATABASE_PATH) -MIGRATION_DIR = importlib.resources.files("simple_web_app").joinpath("migrations") -TEMPLATE_DIR = importlib.resources.files("simple_web_app").joinpath("templates") -QUERY_DIR = importlib.resources.files("simple_web_app").joinpath("queries") -STATIC_DIR = importlib.resources.files("simple_web_app").joinpath("static") - -templates = Jinja2Templates(directory=TEMPLATE_DIR) -queries_basic = aiosql.from_path(QUERY_DIR / "basic.sql", "aiosqlite") - - -def is_htmx_request(request: Request) -> bool: - is_hx_request = request.headers.get("HX-Request") is not None - is_hx_history_restore = ( - request.headers.get("HX-History-Restore-Request") is not None - ) - return is_hx_request or is_hx_history_restore - - -def render( - request: Request, partial_template: str, include_oob: str | None = None, **kwargs -): - ctx = kwargs.get("context", {}) - is_authenticated = False # request.user.is_authenticated if request.user else False - ctx = ctx | {"is_authenticated": is_authenticated} - headers = kwargs.get("headers", {}) - if is_htmx_request(request): - if include_oob: - ctx = ctx | { - "context_partial": partial_template, - "oob_template": include_oob, - } - return templates.TemplateResponse( - request, partial_template, ctx, headers=headers - ) - return templates.TemplateResponse( - request, - "application.html", - ctx | {"content": partial_template}, - headers=headers, - ) - - -def format_timedelta(tdelta, fmt): - d = {"days": tdelta.days} - d["hours"], rem = divmod(tdelta.seconds, 3600) - d["minutes"], d["seconds"] = divmod(rem, 60) - return fmt.format(**d) - - -async def show_home_page(request: Request): - page = request.query_params.get("page", default=0) - page = int(page) if page is not None else page - current_time = request.query_params.get( - "current_time", default=datetime.datetime.now(tz=datetime.UTC) - ) - category_id = request.query_params.get("category_id") - category_id = int(category_id) if category_id is not None else None - - limit = 5 - offset = page * limit - - all_categories = await queries_basic.get_categories(request.state.conn, limit=20) - - rows = ( - await queries_basic.get_news_by_category( - request.state.conn, - category_id=category_id, - limit=limit + 1, - offset=offset, - max_published_time=current_time, - ) - if category_id - else await queries_basic.get_news( - request.state.conn, - limit=limit + 1, - offset=offset, - max_published_time=current_time, - ) - ) - reached_end = len(rows) <= limit - rows = rows[:limit] - categories = await asyncio.gather( - *[ - queries_basic.get_categories_for_news( - request.state.conn, news_item_id=row["id"] - ) - for row in rows - ] - ) - - current_time_utc = datetime.datetime.now(tz=datetime.UTC) - - def _get_time_since_published(published: str, current_time: datetime): - published = datetime.datetime.strptime(published, "%Y-%m-%dT%H:%M:%S.%f%z") - return format_timedelta( - current_time - published, "{days} days, {hours} hours ago" - ) - - times_since_published = [ - _get_time_since_published(row["published"], current_time_utc) for row in rows - ] - - news = [ - {**row, "categories": c, "time_since_published": tsp} - for row, c, tsp in zip(rows, categories, times_since_published, strict=True) - ] - context = { - "news": news, - "categories": all_categories, - "page": page, - "current_time": current_time, - "category_id": category_id, - "reached_end": reached_end, - } - - if is_htmx_request(request): - context = context | {"oob": True} - template_name = "oob_swap.html" if is_htmx_request(request) else "index.html" - return render(request, template_name, context=context) - - -async def open_search(request: Request): - return render(request, "search.html") - - -async def search(request: Request): - async with request.form() as form: - query = form["search"] - rows = ( - await queries_basic.search_news(request.state.conn, query=query, limit=10) - if query - else [] - ) - return render(request, "search_results.html", context={"news": rows}) - - -async def open_settings(request: Request): - tab = request.query_params.get("tab", default="general") - return render(request, "settings.html", context={"tab": tab}) - - -async def open_settings_tab(request: Request): - tab = request.query_params.get("tab", default="general") - return render(request, "settings_tab.html", context={"tab": tab}) - - -class CacheControlMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request, call_next): - response = await call_next(request) - if request.headers.get("HX-Preloaded") == "true": - response.headers["cache-control"] = "private, max-age=60" - return response - - -@contextlib.asynccontextmanager -async def lifespan(app: Starlette): - logger.debug("START LIFESPAN") - async with aiosqlite.connect(DATABASE_PATH, autocommit=True) as conn: - conn.row_factory = aiosqlite.Row - - await create_migrations_table_if_not_exists(conn) - migration_files = sorted(MIGRATION_DIR.glob("*.sql")) - migration_queries = [p.read_text() for p in migration_files] - await apply_migrations(conn, migration_queries) - - cursors = [conn.execute(query) for query in SQLITE_PRAGMAS] - [await c for c in cursors] - logger.debug("FINISH LIFESPAN") - yield {"conn": conn} - logger.debug("KILL LIFESPAN") - - -routes = [ - Route("/", methods=["GET"], endpoint=show_home_page, name="news_page"), - Route("/search", methods=["GET"], endpoint=open_search, name="search_page"), - Route("/search", methods=["POST"], endpoint=search, name="search"), - Route("/settings", methods=["GET"], endpoint=open_settings, name="settings_page"), - Route( - "/settings/tab", - methods=["GET"], - endpoint=open_settings_tab, - name="settings_tab", - ), - Mount("/static", StaticFiles(directory=STATIC_DIR), name="static"), -] -middleware = [ - Middleware(CacheControlMiddleware), - # Middleware(SessionMiddleware, secret_key=SECRET_KEY), -] -app = Starlette( - debug=DEBUG, - routes=routes, - middleware=middleware, - lifespan=lifespan, -) diff --git a/src/simple_web_app/log.py b/src/simple_web_app/log.py @@ -1,59 +0,0 @@ -import logging -import json - -import orjson - - -class JsonFormatter(logging.Formatter): - """ - Formatter that outputs JSON strings after parsing the LogRecord. - - @param dict fmt_dict: Key: logging format attribute pairs. Defaults to {"message": "message"}. - @param str time_format: time.strftime() format string. Default: "%Y-%m-%dT%H:%M:%S" - @param str msec_format: Microsecond formatting. Appended at the end. Default: "%s.%03dZ" - """ - def __init__(self, fmt_dict: dict = None, time_format: str = "%Y-%m-%dT%H:%M:%S", msec_format: str = "%s.%03dZ"): - self.fmt_dict = fmt_dict if fmt_dict is not None else {"message": "message"} - self.default_time_format = time_format - self.default_msec_format = msec_format - self.datefmt = None - - def usesTime(self) -> bool: - """ - Overwritten to look for the attribute in the format dict values instead of the fmt string. - """ - return "asctime" in self.fmt_dict.values() - - def formatMessage(self, record) -> dict: - """ - Overwritten to return a dictionary of the relevant LogRecord attributes instead of a string. - KeyError is raised if an unknown attribute is provided in the fmt_dict. - """ - return {fmt_key: record.__dict__[fmt_val] for fmt_key, fmt_val in self.fmt_dict.items()} - - def format(self, record) -> str: - """ - Mostly the same as the parent's class method, the difference being that a dict is manipulated and dumped as JSON - instead of a string. - """ - record.message = record.getMessage() - - if self.usesTime(): - record.asctime = self.formatTime(record, self.datefmt) - - message_dict = self.formatMessage(record) - - if record.exc_info: - # Cache the traceback text to avoid converting it multiple times - # (it's constant anyway) - if not record.exc_text: - record.exc_text = self.formatException(record.exc_info) - - if record.exc_text: - message_dict["exc_info"] = record.exc_text - - if record.stack_info: - message_dict["stack_info"] = self.formatStack(record.stack_info) - - return json.dumps(message_dict, default=str) - diff --git a/src/simple_web_app/migration.py b/src/simple_web_app/migration.py @@ -1,63 +0,0 @@ -import datetime -import logging -import aiosqlite -from pathlib import Path - -logger = logging.getLogger(__name__) - - -async def create_migrations_table_if_not_exists(conn: aiosqlite.Connection) -> None: - result = await conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='migration_version';") - row = await result.fetchone() - migrations_table_exists = row is not None - if not migrations_table_exists: - logger.info("Creating migrations table migration_version.") - await conn.execute("CREATE TABLE migration_version (version INTEGER)") - await conn.execute("INSERT INTO migration_version (version) VALUES (0)") - await conn.commit() - - -async def apply_migrations(conn: aiosqlite.Connection, migration_queries: list[str]) -> None: - result = await conn.execute("SELECT version FROM migration_version") - (db_version,) = await result.fetchone() - num_migrations = len(migration_queries) - for i in range(db_version, num_migrations): - query = migration_queries[i] - print(f"Running migration {i}: {query}") - await conn.execute("BEGIN TRANSACTION") - try: - _ = await conn.executescript(query) - _ = await conn.execute("UPDATE migration_version SET version = ?", [i + 1]) - await conn.execute("COMMIT TRANSACTION") - except Exception as e: - await conn.execute("ROLLBACK") - logger.exception(f"Encountered the following error while running the migration: {e}") - raise - - - -def create_migration(name: str, migrations_dir: Path): - current_timestamp = datetime.datetime.now(tz=datetime.UTC) - file_name = f"{current_timestamp.strftime('%Y%m%d%H%M%S')}_{name}.sql" - file_path = migrations_dir / file_name - file_path.touch() - return file_path - - -def run_create_migration(): - import argparse - import importlib.resources - - default_migrations_dir = importlib.resources.files("simple_web_app").joinpath("migrations") - parser = argparse.ArgumentParser() - parser.add_argument("name") - parser.add_argument("-d", "--dir", default=default_migrations_dir) - args = parser.parse_args() - - path = create_migration(args.name, Path(args.dir)) - print(str(path)) - - -if __name__ == "__main__": - run_create_migration() - diff --git a/src/simple_web_app/queries/basic.sql b/src/simple_web_app/queries/basic.sql @@ -1,39 +0,0 @@ --- name: get_news(limit, offset, max_published_time) -SELECT id, title, text, published -FROM news_item -WHERE language = 'english' - AND (published < :max_published_time) -ORDER BY published DESC, id ASC -LIMIT :limit -OFFSET :offset; - --- name: search_news(query, limit) -SELECT title, text -FROM news_item_fts(:query) -LIMIT :limit; - --- name: get_news_by_category(category_id, limit, offset, max_published_time) -SELECT ni.id, ni.title, ni.text, ni.published -FROM news_item AS ni -INNER JOIN news_item_category AS nic - ON nic.news_item_id = ni.id -WHERE ni.language = 'english' - AND (ni.published < :max_published_time) - AND nic.category_id = :category_id -ORDER BY ni.published DESC, ni.id ASC -LIMIT :limit -OFFSET :offset; - --- name: get_categories_for_news(news_item_id) -SELECT c.id, c.name -FROM news_item_category AS nic -INNER JOIN category AS c - ON c.id = nic.category_id -WHERE nic.news_item_id = :news_item_id; - --- name: get_categories(limit) -SELECT id, name -FROM category -ORDER BY id -LIMIT :limit; - diff --git a/src/simple_web_app/templates/application.html b/src/simple_web_app/templates/application.html @@ -1,21 +0,0 @@ -<!doctype html> -<html lang="en"> - <head> - <meta charset="utf-8" /> - <title>My Chat</title> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script> - <script src="https://cdn.jsdelivr.net/npm/htmx-ext-preload@2.1.0"></script> - <script src="https://unpkg.com/hyperscript.org@0.9.14"></script> - <link rel="stylesheet" href="{{ url_for('static', path='style.css') }}" /> - <link - rel="stylesheet" - href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.amber.min.css" - /> - </head> - <body hx-ext="preload"> - <main class="container"> - <div id="content">{% include content %}</div> - </main> - </body> -</html> diff --git a/src/simple_web_app/templates/chat.html b/src/simple_web_app/templates/chat.html diff --git a/src/simple_web_app/templates/index.html b/src/simple_web_app/templates/index.html @@ -1,176 +0,0 @@ -<div id="login"> - <!-- <article> --> - <nav> - <ul> - <li> - <a href="/" class="secondary" - ><img src="https://kite.kagi.com/svg/kagi_news_compact_dark.svg" - /></a> - </li> - </ul> - <ul> - <li> - <strong - style="cursor: pointer" - data-tooltip="Choose another date" - data-placement="bottom" - >Tuesday, September 30</strong - > - </li> - </ul> - <ul> - <li> - <a - id="search-btn" - style="cursor: pointer" - data-tooltip="Search" - hx-trigger="click, keyup[ctrlKey&&shiftKey&&key=='O']" - hx-get="{{ url_for('search_page') }}" - hx-target="#modal-placeholder" - hx-swap="innerHTML" - preload - class="secondary" - ><svg - height="20pt" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 640 640" - > - <!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--> - <path - d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM272 416C351.5 416 416 351.5 416 272C416 192.5 351.5 128 272 128C192.5 128 128 192.5 128 272C128 351.5 192.5 416 272 416z" - /></svg - ></a> - </li> - <li> - <a id="font-size-btn" data-tooltip="Change font size" class="secondary" - ><svg - height="20pt" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 640 640" - > - <!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--> - <path - d="M349.1 114.7C343.9 103.3 332.5 96 320 96C307.5 96 296.1 103.3 290.9 114.7L123.5 480L112 480C94.3 480 80 494.3 80 512C80 529.7 94.3 544 112 544L200 544C217.7 544 232 529.7 232 512C232 494.3 217.7 480 200 480L193.9 480L215.9 432L424.2 432L446.2 480L440.1 480C422.4 480 408.1 494.3 408.1 512C408.1 529.7 422.4 544 440.1 544L528.1 544C545.8 544 560.1 529.7 560.1 512C560.1 494.3 545.8 480 528.1 480L516.6 480L349.2 114.7zM394.8 368L245.2 368L320 204.8L394.8 368z" - /></svg - ></a> - </li> - <li> - <a - id="settings-btn" - style="cursor: pointer" - data-tooltip="Settings" - hx-get="{{ url_for('settings_page') }}" - hx-target="#modal-placeholder" - hx-swap="innerHTML" - preload - class="secondary" - ><svg - height="20pt" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 640 640" - > - <!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--> - <path - d="M259.1 73.5C262.1 58.7 275.2 48 290.4 48L350.2 48C365.4 48 378.5 58.7 381.5 73.5L396 143.5C410.1 149.5 423.3 157.2 435.3 166.3L503.1 143.8C517.5 139 533.3 145 540.9 158.2L570.8 210C578.4 223.2 575.7 239.8 564.3 249.9L511 297.3C511.9 304.7 512.3 312.3 512.3 320C512.3 327.7 511.8 335.3 511 342.7L564.4 390.2C575.8 400.3 578.4 417 570.9 430.1L541 481.9C533.4 495 517.6 501.1 503.2 496.3L435.4 473.8C423.3 482.9 410.1 490.5 396.1 496.6L381.7 566.5C378.6 581.4 365.5 592 350.4 592L290.6 592C275.4 592 262.3 581.3 259.3 566.5L244.9 496.6C230.8 490.6 217.7 482.9 205.6 473.8L137.5 496.3C123.1 501.1 107.3 495.1 99.7 481.9L69.8 430.1C62.2 416.9 64.9 400.3 76.3 390.2L129.7 342.7C128.8 335.3 128.4 327.7 128.4 320C128.4 312.3 128.9 304.7 129.7 297.3L76.3 249.8C64.9 239.7 62.3 223 69.8 209.9L99.7 158.1C107.3 144.9 123.1 138.9 137.5 143.7L205.3 166.2C217.4 157.1 230.6 149.5 244.6 143.4L259.1 73.5zM320.3 400C364.5 399.8 400.2 363.9 400 319.7C399.8 275.5 363.9 239.8 319.7 240C275.5 240.2 239.8 276.1 240 320.3C240.2 364.5 276.1 400.2 320.3 400z" - /></svg - ></a> - </li> - </ul> - </nav> - - <div class="overflow-auto" style="text-wrap: nowrap"> - <table> - <tr> - {% for category in categories %} - <th> - <div - hx-get="{{ url_for('news_page').include_query_params(category_id=category.id) }}" - hx-target="#news-cards" - preload - style="cursor: pointer" - > - {{ category.name }} - </div> - </th> - {% endfor %} - </tr> - </table> - </div> - - <div id="modal-placeholder"></div> - <div id="news-cards-with-buttons"> - <div id="news-cards">{% include 'news_cards.html' %}</div> - <nav> - <ul></ul> - <ul> - <li>{% include 'load_more.html' %}</li> - <li><button>Mark all as read</button></li> - </ul> - <ul></ul> - </nav> - </div> - <!-- </article> --> - <footer> - <nav> - <ul></ul> - <ul> - <li> - <a href="https://github.com/silasbrack/simple-web-app" - ><svg - height="24pt" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 640 640" - > - <!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--> - <path - d="M237.9 461.4C237.9 463.4 235.6 465 232.7 465C229.4 465.3 227.1 463.7 227.1 461.4C227.1 459.4 229.4 457.8 232.3 457.8C235.3 457.5 237.9 459.1 237.9 461.4zM206.8 456.9C206.1 458.9 208.1 461.2 211.1 461.8C213.7 462.8 216.7 461.8 217.3 459.8C217.9 457.8 216 455.5 213 454.6C210.4 453.9 207.5 454.9 206.8 456.9zM251 455.2C248.1 455.9 246.1 457.8 246.4 460.1C246.7 462.1 249.3 463.4 252.3 462.7C255.2 462 257.2 460.1 256.9 458.1C256.6 456.2 253.9 454.9 251 455.2zM316.8 72C178.1 72 72 177.3 72 316C72 426.9 141.8 521.8 241.5 555.2C254.3 557.5 258.8 549.6 258.8 543.1C258.8 536.9 258.5 502.7 258.5 481.7C258.5 481.7 188.5 496.7 173.8 451.9C173.8 451.9 162.4 422.8 146 415.3C146 415.3 123.1 399.6 147.6 399.9C147.6 399.9 172.5 401.9 186.2 425.7C208.1 464.3 244.8 453.2 259.1 446.6C261.4 430.6 267.9 419.5 275.1 412.9C219.2 406.7 162.8 398.6 162.8 302.4C162.8 274.9 170.4 261.1 186.4 243.5C183.8 237 175.3 210.2 189 175.6C209.9 169.1 258 202.6 258 202.6C278 197 299.5 194.1 320.8 194.1C342.1 194.1 363.6 197 383.6 202.6C383.6 202.6 431.7 169 452.6 175.6C466.3 210.3 457.8 237 455.2 243.5C471.2 261.2 481 275 481 302.4C481 398.9 422.1 406.6 366.2 412.9C375.4 420.8 383.2 435.8 383.2 459.3C383.2 493 382.9 534.7 382.9 542.9C382.9 549.4 387.5 557.3 400.2 555C500.2 521.8 568 426.9 568 316C568 177.3 455.5 72 316.8 72zM169.2 416.9C167.9 417.9 168.2 420.2 169.9 422.1C171.5 423.7 173.8 424.4 175.1 423.1C176.4 422.1 176.1 419.8 174.4 417.9C172.8 416.3 170.5 415.6 169.2 416.9zM158.4 408.8C157.7 410.1 158.7 411.7 160.7 412.7C162.3 413.7 164.3 413.4 165 412C165.7 410.7 164.7 409.1 162.7 408.1C160.7 407.5 159.1 407.8 158.4 408.8zM190.8 444.4C189.2 445.7 189.8 448.7 192.1 450.6C194.4 452.9 197.3 453.2 198.6 451.6C199.9 450.3 199.3 447.3 197.3 445.4C195.1 443.1 192.1 442.8 190.8 444.4zM179.4 429.7C177.8 430.7 177.8 433.3 179.4 435.6C181 437.9 183.7 438.9 185 437.9C186.6 436.6 186.6 434 185 431.7C183.6 429.4 181 428.4 179.4 429.7z" - /> - </svg> - Contribute</a - > - </li> - - <li> - <a href="" - ><svg - height="20pt" - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - stroke-width="1.5" - stroke="currentColor" - class="size-6" - > - <path - stroke-linecap="round" - stroke-linejoin="round" - d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" - /> - </svg> - About</a - > - </li> - - <li> - <a href="" - ><svg - height="20pt" - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 24 24" - fill="currentColor" - class="size-6" - > - <path - fill-rule="evenodd" - d="M3.75 4.5a.75.75 0 0 1 .75-.75h.75c8.284 0 15 6.716 15 15v.75a.75.75 0 0 1-.75.75h-.75a.75.75 0 0 1-.75-.75v-.75C18 11.708 12.292 6 5.25 6H4.5a.75.75 0 0 1-.75-.75V4.5Zm0 6.75a.75.75 0 0 1 .75-.75h.75a8.25 8.25 0 0 1 8.25 8.25v.75a.75.75 0 0 1-.75.75H12a.75.75 0 0 1-.75-.75v-.75a6 6 0 0 0-6-6H4.5a.75.75 0 0 1-.75-.75v-.75Zm0 7.5a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Z" - clip-rule="evenodd" - /> - </svg> - RSS Feed</a - > - </li> - </ul> - <ul></ul> - </nav> - </footer> -</div> diff --git a/src/simple_web_app/templates/load_more.html b/src/simple_web_app/templates/load_more.html @@ -1,24 +0,0 @@ -<button - id="load-more-btn" - {%- - if - reached_end - %} - disabled{% - endif - %} - {% - if - oob - %}hx-swap-oob="true" - {% - endif - %} - hx-target="#news-cards" - hx-swap="beforeend" - hx-get="{{ url_for('news_page') }}?page={{ page + 1 }}&current_time={{ current_time }}{% if category_id %}&category_id={{ category_id }}{% endif %}" - preload - class="secondary" -> - Load more -</button> diff --git a/src/simple_web_app/templates/news_card.html b/src/simple_web_app/templates/news_card.html @@ -1,30 +0,0 @@ -<header> - <nav> - <ul> - {% for category in new.categories %} - <li> - <div - style="cursor: pointer" - hx-get="{{ url_for('news_page').include_query_params(category_id=category.id) }}" - hx-target="#news-cards" - preload - > - <small>{{ category.name }}</small> - </div> - </li> - {% endfor %} - </ul> - </nav> -</header> -<details> - <summary> - <strong>{{ new.title }}</strong> - </summary> - <p><small>{{ new.text }}</small></p> -</details> -<footer> - <small> - <em> {{ new.time_since_published }} </em> - </small> -</footer> -<hr /> diff --git a/src/simple_web_app/templates/news_cards.html b/src/simple_web_app/templates/news_cards.html @@ -1 +0,0 @@ -{% for new in news %} {% include 'news_card.html' %} {% endfor %} diff --git a/src/simple_web_app/templates/oob_swap.html b/src/simple_web_app/templates/oob_swap.html @@ -1 +0,0 @@ -{% include 'news_cards.html' %} {% include 'load_more.html' %} diff --git a/src/simple_web_app/templates/settings.html b/src/simple_web_app/templates/settings.html @@ -1,96 +0,0 @@ -<div - id="settings-modal" - class="modal-is-open modal-is-opening" - _="on closeModal remove .modal-is-opening then wait 10 then add .modal-is-closing then wait for animationend then remove me" -> - <dialog _="on click if target is me then trigger closeModal" open> - <article> - <div class="overflow-auto" style="text-wrap: nowrap"> - <nav> - <ul> - <li> - <div - id="general-tab" - hx-get="{{ url_for('settings_tab').include_query_params(tab='general') }}" - hx-target="#settings-tab" - hx-swap="innerHTML" - preload - style="cursor: pointer" - > - General - </div> - </li> - <li> - <div - id="categories-tab" - hx-get="{{ url_for('settings_tab').include_query_params(tab='categories') }}" - hx-target="#settings-tab" - hx-swap="innerHTML" - preload - style="cursor: pointer" - > - Categories - </div> - </li> - <li> - <div - id="sections-tab" - hx-get="{{ url_for('settings_tab').include_query_params(tab='sections') }}" - hx-target="#settings-tab" - hx-swap="innerHTML" - preload - style="cursor: pointer" - > - Sections - </div> - </li> - <li> - <div - id="content-filter-tab" - hx-get="{{ url_for('settings_tab').include_query_params(tab='content-filter') }}" - hx-target="#settings-tab" - hx-swap="innerHTML" - preload - style="cursor: pointer" - > - Content Filter - </div> - </li> - <li> - <div - id="syncing-tab" - hx-get="{{ url_for('settings_tab').include_query_params(tab='syncing') }}" - hx-target="#settings-tab" - hx-swap="innerHTML" - preload - style="cursor: pointer" - > - Syncing - </div> - </li> - <li> - <div - id="experimental-tab" - hx-get="{{ url_for('settings_tab').include_query_params(tab='experimental') }}" - hx-target="#settings-tab" - hx-swap="innerHTML" - preload - style="cursor: pointer" - > - Experimental - </div> - </li> - </ul> - </nav> - </div> - <hr /> - <div id="settings-tab">{% include 'settings_tab.html' %}</div> - <footer> - <button _="on click trigger closeModal" class="secondary"> - Cancel - </button> - <button>Save</button> - </footer> - </article> - </dialog> -</div> diff --git a/src/simple_web_app/templates/settings_tab.html b/src/simple_web_app/templates/settings_tab.html @@ -1,501 +0,0 @@ -<section> - {% if tab == "general" %} - <h4>Appearance</h4> - - <label> - Theme - <details class="dropdown"> - <summary>System</summary> - <ul> - <li><a>System</a></li> - <li><a>Light</a></li> - <li><a>Dark</a></li> - </ul> - </details> - </label> - - <label> - Font Size - <details class="dropdown"> - <summary>Normal</summary> - <ul> - <li><a href="#">Small</a></li> - <li><a href="#">Normal</a></li> - <li><a href="#">Large</a></li> - </ul> - </details> - </label> - - <h4>Language & Region</h4> - - <label> - Interface Language - <details class="dropdown"> - <summary>English</summary> - <ul> - <li>English</li> - <li>Spanish</li> - <li>Danish</li> - <li>Chinese</li> - </ul> - </details> - </label> - - <label> - Content Language - <details class="dropdown"> - <summary>English</summary> - <ul> - <li>English</li> - <li>Spanish</li> - <li>Danish</li> - <li>Chinese</li> - </ul> - </details> - </label> - - <h4>Reading Experience</h4> - - <label> - Number of Stories Shown: <output>10</output> - <input - type="range" - value="10" - min="1" - max="30" - oninput="this.previousElementSibling.value = this.value" - /> - </label> - - <label> - Story Open Mode - <details class="dropdown"> - <summary>Multiple stories</summary> - <ul> - <li>Multiple stories</li> - <li>One story at a time</li> - </ul> - </details> - </label> - - <label> - Story Expand Mode - <details class="dropdown"> - <summary>Double-click to expand all</summary> - <ul> - <li>Always expand all</li> - <li>Double-click to expand all</li> - <li>Never expand all</li> - </ul> - </details> - </label> - - <h4>Navigation</h4> - - <label> - Use Latest URLs - <details class="dropdown"> - <summary>Disabled</summary> - <ul> - <li>Disabled</li> - <li>Enabled</li> - </ul> - </details> - </label> - - <h4>About</h4> - - <label> - Maps Provider - <details class="dropdown"> - <summary>Auto</summary> - <ul> - <li>Auto</li> - <li>Kagi Maps</li> - <li>Google Maps</li> - <li>OpenStreetMap</li> - <li>Apple Maps</li> - </ul> - </details> - </label> - {% elif tab == "sections" %} - <hgroup> - <h4>Article Sections</h4> - <h6>Drag to reorder sections. Use the switch to enable/disable.</h6> - </hgroup> - <article> - <nav> - <ul> - <li>Summary</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Primary Image</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Sources</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Highlights</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Quotes</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Secondary Image</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Perspectives</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Historical Background</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Humanitarian Impact</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Technical Details</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Business Angle</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Scientific Significance</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li>Travel Advisory</li> - </ul> - <ul> - <li> - <label><input name="summary" type="checkbox" role="switch" /></label> - </li> - </ul> - </nav> - </article> - <nav> - <ul></ul> - <ul> - <li> - <button class="secondary">Reset Order</button> - <button>Toggle All</button> - </li> - </ul> - <ul></ul> - </nav> - {% elif tab == "content-filter" %} - <hgroup> - <h4>Filter Presets</h4> - <p>Select one or more preset filters to quickly hide common topics</p> - </hgroup> - <select aria-label="Topics" multiple size="6"> - <option>Cheese</option> - <option selected>Fruits</option> - <option selected>Nuts</option> - <option>Chocolate</option> - <option>Crackers</option> - </select> - <hgroup> - <h4>Custom Keywords</h4> - <p>Add your own keywords to filter, separated by commas</p> - </hgroup> - <input - type="text" - name="custom-keywords" - placeholder="e.g., celebrity name, topic" - aria-label="Custom Keywords" - /> - <hgroup> - <h4>Filter Mode</h4> - <p>Choose how filtered content is handled</p> - </hgroup> - <fieldset> - <label> - <input type="radio" name="language" checked /> - Hide completely - </label> - <label> - <input type="radio" name="language" /> - Blur with warning - </label> - </fieldset> - <hgroup> - <h4>Filter Scope</h4> - <p>Choose which parts of stories to check for keywords</p> - </hgroup> - <select name="filter-scope" required> - <option>Title only</option> - <option>Title and summary</option> - <option selected>All content</option> - </select> - <nav> - <ul> - <li> - <hgroup> - <h4>Show Filtered Count</h4> - <p>Display number of filtered stories in each category</p> - </hgroup> - </li> - </ul> - <ul> - <li> - <input name="terms" type="checkbox" role="switch" /> - </li> - </ul> - </nav> - <hr /> - <hgroup> - <h4>Backup & Restore</h4> - <p> - Export your filter settings to a file or import from a previous backup - </p> - </hgroup> - <button class="secondary">Export Settings</button> - <button>Import Settings</button> - <hr /> - <button class="outline">Reset Content Filter Settings to Defaults</button> - {% elif tab == "syncing" %} - <p> - Kagi News can sync your settings and read history across all your devices. - This data is stored securely on Kagi servers and associated with your Kagi - Search account. Your synced data is not used for any other purpose, not - shared with anyone, and is solely stored to provide the sync service to you. - You have full control over what gets synced and can delete your data at any - time. - </p> - <h4>Sync Preferences</h4> - <label> - <input - name="sync-settings" - type="checkbox" - role="switch" - aria-describedby="sync-settings-description" - /> - Sync Settings - </label> - <label> - <input - name="sync-read-history" - type="checkbox" - role="switch" - aria-describedby="email-helper" - /> - Sync Read History - </label> - <br /> - <h4>Data Management</h4> - <p> - You are in control of your data. Export or delete your cloud data at any - time. Deletion does not affect local data. - </p> - <button class="secondary">Export Data</button> - <button>Clear All Synced Data</button> - {% elif tab == "experimental" %} - <p> - ⚠️ These are experimental features that may be changed or removed at any - time. - </p> - <article> - <nav> - <ul> - <li> - <hgroup> - <h4>Show icons next to articles</h4> - <p> - When enabled, small icons will be displayed next to articles to - provide visual context. - </p> - </hgroup> - </li> - </ul> - <ul> - <li> - <input - name="show-icons-next-to-articles" - type="checkbox" - role="switch" - /> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li> - <hgroup> - <h4>Show icons in category labels</h4> - <p> - When enabled, small icons will be displayed next to category - labels to provide visual context. - </p> - </hgroup> - </li> - </ul> - <ul> - <li> - <input - name="show-icons-next-to-category-labels" - type="checkbox" - role="switch" - /> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li> - <hgroup> - <h4>Disable horizontal category swiping</h4> - <p> - When enabled, horizontal swiping to change categories on mobile - devices will be disabled. - </p> - </hgroup> - </li> - </ul> - <ul> - <li> - <input - name="disable-horizontal-category-swiping" - type="checkbox" - role="switch" - /> - </li> - </ul> - </nav> - </article> - <article> - <nav> - <ul> - <li> - <hgroup> - <h4>Show World Tension Index</h4> - <p> - Display a global temperature reading of world stability based on - current events. - </p> - </hgroup> - </li> - </ul> - <ul> - <li> - <input - name="show-icons-next-to-articles" - type="checkbox" - role="switch" - /> - </li> - </ul> - </nav> - </article> - {% endif %} -</section> diff --git a/src/state.rs b/src/state.rs @@ -0,0 +1,39 @@ +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use sqlx::SqlitePool; +use std::str::FromStr; + +use crate::config::Config; + +#[derive(Clone)] +pub struct AppState { + pub pool: SqlitePool, +} + +impl AppState { + pub async fn new(config: &Config) -> Result<Self, sqlx::Error> { + let options = SqliteConnectOptions::from_str(&config.database_url)? + .create_if_missing(true) + .journal_mode(sqlx::sqlite::SqliteJournalMode::Wal) + .foreign_keys(true) + .synchronous(sqlx::sqlite::SqliteSynchronous::Normal) + .busy_timeout(std::time::Duration::from_secs(5)); + + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with(options) + .await?; + + // Run additional pragmas + sqlx::query("PRAGMA cache_size = 1000000000") + .execute(&pool) + .await?; + sqlx::query("PRAGMA temp_store = MEMORY") + .execute(&pool) + .await?; + + // Run migrations + sqlx::migrate!("./migrations").run(&pool).await?; + + Ok(Self { pool }) + } +} diff --git a/src/templates.rs b/src/templates.rs @@ -0,0 +1,76 @@ +use askama::Template; + +use crate::db::{Category, NewsWithCategories, SearchResult}; + +#[derive(Template)] +#[template(path = "application.html")] +pub struct ApplicationTemplate { + pub content: String, +} + +#[derive(Template)] +#[template(path = "index.html")] +pub struct IndexTemplate { + pub news: Vec<NewsWithCategories>, + pub categories: Vec<Category>, + pub page: i64, + pub current_time: String, + pub category_id: Option<i64>, + pub reached_end: bool, + pub oob: bool, +} + +#[derive(Template)] +#[template(path = "oob_swap.html")] +pub struct OobSwapTemplate { + pub news: Vec<NewsWithCategories>, + pub page: i64, + pub current_time: String, + pub category_id: Option<i64>, + pub reached_end: bool, + pub oob: bool, +} + +#[derive(Template)] +#[template(path = "news_cards.html")] +pub struct NewsCardsTemplate<'a> { + pub news: &'a [NewsWithCategories], +} + +#[derive(Template)] +#[template(path = "news_card.html")] +pub struct NewsCardTemplate<'a> { + pub new: &'a NewsWithCategories, +} + +#[derive(Template)] +#[template(path = "load_more.html")] +pub struct LoadMoreTemplate { + pub page: i64, + pub current_time: String, + pub category_id: Option<i64>, + pub reached_end: bool, + pub oob: bool, +} + +#[derive(Template)] +#[template(path = "search.html")] +pub struct SearchTemplate; + +#[derive(Template)] +#[template(path = "search_results.html")] +pub struct SearchResultsTemplate { + pub news: Vec<SearchResult>, +} + +#[derive(Template)] +#[template(path = "settings.html")] +pub struct SettingsTemplate { + pub tab: String, +} + +#[derive(Template)] +#[template(path = "settings_tab.html")] +pub struct SettingsTabTemplate { + pub tab: String, +} diff --git a/src/simple_web_app/static/style.css b/static/style.css diff --git a/templates/application.html b/templates/application.html @@ -0,0 +1,21 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <title>My Chat</title> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script> + <script src="https://cdn.jsdelivr.net/npm/htmx-ext-preload@2.1.0"></script> + <script src="https://unpkg.com/hyperscript.org@0.9.14"></script> + <link rel="stylesheet" href="/static/style.css" /> + <link + rel="stylesheet" + href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.amber.min.css" + /> + </head> + <body hx-ext="preload"> + <main class="container"> + <div id="content">{{ content|safe }}</div> + </main> + </body> +</html> diff --git a/templates/index.html b/templates/index.html @@ -0,0 +1,180 @@ +<div id="login"> + <!-- <article> --> + <nav> + <ul> + <li> + <a href="/" class="secondary" + ><img src="https://kite.kagi.com/svg/kagi_news_compact_dark.svg" + /></a> + </li> + </ul> + <ul> + <li> + <strong + style="cursor: pointer" + data-tooltip="Choose another date" + data-placement="bottom" + >Tuesday, September 30</strong + > + </li> + </ul> + <ul> + <li> + <a + id="search-btn" + style="cursor: pointer" + data-tooltip="Search" + hx-trigger="click, keyup[ctrlKey&&shiftKey&&key=='O']" + hx-get="/search" + hx-target="#modal-placeholder" + hx-swap="innerHTML" + preload + class="secondary" + ><svg + height="20pt" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 640 640" + > + <!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--> + <path + d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM272 416C351.5 416 416 351.5 416 272C416 192.5 351.5 128 272 128C192.5 128 128 192.5 128 272C128 351.5 192.5 416 272 416z" + /></svg + ></a> + </li> + <li> + <a id="font-size-btn" data-tooltip="Change font size" class="secondary" + ><svg + height="20pt" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 640 640" + > + <!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--> + <path + d="M349.1 114.7C343.9 103.3 332.5 96 320 96C307.5 96 296.1 103.3 290.9 114.7L123.5 480L112 480C94.3 480 80 494.3 80 512C80 529.7 94.3 544 112 544L200 544C217.7 544 232 529.7 232 512C232 494.3 217.7 480 200 480L193.9 480L215.9 432L424.2 432L446.2 480L440.1 480C422.4 480 408.1 494.3 408.1 512C408.1 529.7 422.4 544 440.1 544L528.1 544C545.8 544 560.1 529.7 560.1 512C560.1 494.3 545.8 480 528.1 480L516.6 480L349.2 114.7zM394.8 368L245.2 368L320 204.8L394.8 368z" + /></svg + ></a> + </li> + <li> + <a + id="settings-btn" + style="cursor: pointer" + data-tooltip="Settings" + hx-get="/settings" + hx-target="#modal-placeholder" + hx-swap="innerHTML" + preload + class="secondary" + ><svg + height="20pt" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 640 640" + > + <!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--> + <path + d="M259.1 73.5C262.1 58.7 275.2 48 290.4 48L350.2 48C365.4 48 378.5 58.7 381.5 73.5L396 143.5C410.1 149.5 423.3 157.2 435.3 166.3L503.1 143.8C517.5 139 533.3 145 540.9 158.2L570.8 210C578.4 223.2 575.7 239.8 564.3 249.9L511 297.3C511.9 304.7 512.3 312.3 512.3 320C512.3 327.7 511.8 335.3 511 342.7L564.4 390.2C575.8 400.3 578.4 417 570.9 430.1L541 481.9C533.4 495 517.6 501.1 503.2 496.3L435.4 473.8C423.3 482.9 410.1 490.5 396.1 496.6L381.7 566.5C378.6 581.4 365.5 592 350.4 592L290.6 592C275.4 592 262.3 581.3 259.3 566.5L244.9 496.6C230.8 490.6 217.7 482.9 205.6 473.8L137.5 496.3C123.1 501.1 107.3 495.1 99.7 481.9L69.8 430.1C62.2 416.9 64.9 400.3 76.3 390.2L129.7 342.7C128.8 335.3 128.4 327.7 128.4 320C128.4 312.3 128.9 304.7 129.7 297.3L76.3 249.8C64.9 239.7 62.3 223 69.8 209.9L99.7 158.1C107.3 144.9 123.1 138.9 137.5 143.7L205.3 166.2C217.4 157.1 230.6 149.5 244.6 143.4L259.1 73.5zM320.3 400C364.5 399.8 400.2 363.9 400 319.7C399.8 275.5 363.9 239.8 319.7 240C275.5 240.2 239.8 276.1 240 320.3C240.2 364.5 276.1 400.2 320.3 400z" + /></svg + ></a> + </li> + </ul> + </nav> + + <div class="overflow-auto" style="text-wrap: nowrap"> + <table> + <tr> + {% for category in categories %} + <th> + <div + hx-get="/?category_id={{ category.id }}" + hx-target="#news-cards" + preload + style="cursor: pointer" + > + {% match category.name %} + {% when Some with (name) %} + {{ name }} + {% when None %} + {% endmatch %} + </div> + </th> + {% endfor %} + </tr> + </table> + </div> + + <div id="modal-placeholder"></div> + <div id="news-cards-with-buttons"> + <div id="news-cards">{% include "news_cards.html" %}</div> + <nav> + <ul></ul> + <ul> + <li>{% include "load_more.html" %}</li> + <li><button>Mark all as read</button></li> + </ul> + <ul></ul> + </nav> + </div> + <!-- </article> --> + <footer> + <nav> + <ul></ul> + <ul> + <li> + <a href="https://github.com/silasbrack/simple-web-app" + ><svg + height="24pt" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 640 640" + > + <!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--> + <path + d="M237.9 461.4C237.9 463.4 235.6 465 232.7 465C229.4 465.3 227.1 463.7 227.1 461.4C227.1 459.4 229.4 457.8 232.3 457.8C235.3 457.5 237.9 459.1 237.9 461.4zM206.8 456.9C206.1 458.9 208.1 461.2 211.1 461.8C213.7 462.8 216.7 461.8 217.3 459.8C217.9 457.8 216 455.5 213 454.6C210.4 453.9 207.5 454.9 206.8 456.9zM251 455.2C248.1 455.9 246.1 457.8 246.4 460.1C246.7 462.1 249.3 463.4 252.3 462.7C255.2 462 257.2 460.1 256.9 458.1C256.6 456.2 253.9 454.9 251 455.2zM316.8 72C178.1 72 72 177.3 72 316C72 426.9 141.8 521.8 241.5 555.2C254.3 557.5 258.8 549.6 258.8 543.1C258.8 536.9 258.5 502.7 258.5 481.7C258.5 481.7 188.5 496.7 173.8 451.9C173.8 451.9 162.4 422.8 146 415.3C146 415.3 123.1 399.6 147.6 399.9C147.6 399.9 172.5 401.9 186.2 425.7C208.1 464.3 244.8 453.2 259.1 446.6C261.4 430.6 267.9 419.5 275.1 412.9C219.2 406.7 162.8 398.6 162.8 302.4C162.8 274.9 170.4 261.1 186.4 243.5C183.8 237 175.3 210.2 189 175.6C209.9 169.1 258 202.6 258 202.6C278 197 299.5 194.1 320.8 194.1C342.1 194.1 363.6 197 383.6 202.6C383.6 202.6 431.7 169 452.6 175.6C466.3 210.3 457.8 237 455.2 243.5C471.2 261.2 481 275 481 302.4C481 398.9 422.1 406.6 366.2 412.9C375.4 420.8 383.2 435.8 383.2 459.3C383.2 493 382.9 534.7 382.9 542.9C382.9 549.4 387.5 557.3 400.2 555C500.2 521.8 568 426.9 568 316C568 177.3 455.5 72 316.8 72zM169.2 416.9C167.9 417.9 168.2 420.2 169.9 422.1C171.5 423.7 173.8 424.4 175.1 423.1C176.4 422.1 176.1 419.8 174.4 417.9C172.8 416.3 170.5 415.6 169.2 416.9zM158.4 408.8C157.7 410.1 158.7 411.7 160.7 412.7C162.3 413.7 164.3 413.4 165 412C165.7 410.7 164.7 409.1 162.7 408.1C160.7 407.5 159.1 407.8 158.4 408.8zM190.8 444.4C189.2 445.7 189.8 448.7 192.1 450.6C194.4 452.9 197.3 453.2 198.6 451.6C199.9 450.3 199.3 447.3 197.3 445.4C195.1 443.1 192.1 442.8 190.8 444.4zM179.4 429.7C177.8 430.7 177.8 433.3 179.4 435.6C181 437.9 183.7 438.9 185 437.9C186.6 436.6 186.6 434 185 431.7C183.6 429.4 181 428.4 179.4 429.7z" + /> + </svg> + Contribute</a + > + </li> + + <li> + <a href="" + ><svg + height="20pt" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" + /> + </svg> + About</a + > + </li> + + <li> + <a href="" + ><svg + height="20pt" + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-6" + > + <path + fill-rule="evenodd" + d="M3.75 4.5a.75.75 0 0 1 .75-.75h.75c8.284 0 15 6.716 15 15v.75a.75.75 0 0 1-.75.75h-.75a.75.75 0 0 1-.75-.75v-.75C18 11.708 12.292 6 5.25 6H4.5a.75.75 0 0 1-.75-.75V4.5Zm0 6.75a.75.75 0 0 1 .75-.75h.75a8.25 8.25 0 0 1 8.25 8.25v.75a.75.75 0 0 1-.75.75H12a.75.75 0 0 1-.75-.75v-.75a6 6 0 0 0-6-6H4.5a.75.75 0 0 1-.75-.75v-.75Zm0 7.5a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0Z" + clip-rule="evenodd" + /> + </svg> + RSS Feed</a + > + </li> + </ul> + <ul></ul> + </nav> + </footer> +</div> diff --git a/templates/load_more.html b/templates/load_more.html @@ -0,0 +1,12 @@ +<button + id="load-more-btn" + {% if reached_end %}disabled{% endif %} + {% if oob %}hx-swap-oob="true"{% endif %} + hx-target="#news-cards" + hx-swap="beforeend" + hx-get="/?page={{ page + 1 }}&amp;current_time={{ current_time }}{% if let Some(cat_id) = category_id %}&amp;category_id={{ cat_id }}{% endif %}" + preload + class="secondary" +> + Load more +</button> diff --git a/templates/news_card.html b/templates/news_card.html @@ -0,0 +1,30 @@ +<header> + <nav> + <ul> + {% for category in new.categories %} + <li> + <div + style="cursor: pointer" + hx-get="/?category_id={{ category.id }}" + hx-target="#news-cards" + preload + > + <small>{% match category.name %}{% when Some with (name) %}{{ name }}{% when None %}{% endmatch %}</small> + </div> + </li> + {% endfor %} + </ul> + </nav> +</header> +<details> + <summary> + <strong>{{ new.title }}</strong> + </summary> + <p><small>{{ new.text }}</small></p> +</details> +<footer> + <small> + <em> {{ new.time_since_published }} </em> + </small> +</footer> +<hr /> diff --git a/templates/news_cards.html b/templates/news_cards.html @@ -0,0 +1 @@ +{% for new in news %}{% include "news_card.html" %}{% endfor %} diff --git a/templates/oob_swap.html b/templates/oob_swap.html @@ -0,0 +1 @@ +{% include "news_cards.html" %}{% include "load_more.html" %} diff --git a/src/simple_web_app/templates/search.html b/templates/search.html diff --git a/src/simple_web_app/templates/search_results.html b/templates/search_results.html diff --git a/templates/settings.html b/templates/settings.html @@ -0,0 +1,96 @@ +<div + id="settings-modal" + class="modal-is-open modal-is-opening" + _="on closeModal remove .modal-is-opening then wait 10 then add .modal-is-closing then wait for animationend then remove me" +> + <dialog _="on click if target is me then trigger closeModal" open> + <article> + <div class="overflow-auto" style="text-wrap: nowrap"> + <nav> + <ul> + <li> + <div + id="general-tab" + hx-get="/settings/tab?tab=general" + hx-target="#settings-tab" + hx-swap="innerHTML" + preload + style="cursor: pointer" + > + General + </div> + </li> + <li> + <div + id="categories-tab" + hx-get="/settings/tab?tab=categories" + hx-target="#settings-tab" + hx-swap="innerHTML" + preload + style="cursor: pointer" + > + Categories + </div> + </li> + <li> + <div + id="sections-tab" + hx-get="/settings/tab?tab=sections" + hx-target="#settings-tab" + hx-swap="innerHTML" + preload + style="cursor: pointer" + > + Sections + </div> + </li> + <li> + <div + id="content-filter-tab" + hx-get="/settings/tab?tab=content-filter" + hx-target="#settings-tab" + hx-swap="innerHTML" + preload + style="cursor: pointer" + > + Content Filter + </div> + </li> + <li> + <div + id="syncing-tab" + hx-get="/settings/tab?tab=syncing" + hx-target="#settings-tab" + hx-swap="innerHTML" + preload + style="cursor: pointer" + > + Syncing + </div> + </li> + <li> + <div + id="experimental-tab" + hx-get="/settings/tab?tab=experimental" + hx-target="#settings-tab" + hx-swap="innerHTML" + preload + style="cursor: pointer" + > + Experimental + </div> + </li> + </ul> + </nav> + </div> + <hr /> + <div id="settings-tab">{% include "settings_tab.html" %}</div> + <footer> + <button _="on click trigger closeModal" class="secondary"> + Cancel + </button> + <button>Save</button> + </footer> + </article> + </dialog> +</div> diff --git a/templates/settings_tab.html b/templates/settings_tab.html @@ -0,0 +1,503 @@ +<section> + {% if tab == "general" %} + <h4>Appearance</h4> + + <label> + Theme + <details class="dropdown"> + <summary>System</summary> + <ul> + <li><a>System</a></li> + <li><a>Light</a></li> + <li><a>Dark</a></li> + </ul> + </details> + </label> + + <label> + Font Size + <details class="dropdown"> + <summary>Normal</summary> + <ul> + <li><a href="#">Small</a></li> + <li><a href="#">Normal</a></li> + <li><a href="#">Large</a></li> + </ul> + </details> + </label> + + <h4>Language & Region</h4> + + <label> + Interface Language + <details class="dropdown"> + <summary>English</summary> + <ul> + <li>English</li> + <li>Spanish</li> + <li>Danish</li> + <li>Chinese</li> + </ul> + </details> + </label> + + <label> + Content Language + <details class="dropdown"> + <summary>English</summary> + <ul> + <li>English</li> + <li>Spanish</li> + <li>Danish</li> + <li>Chinese</li> + </ul> + </details> + </label> + + <h4>Reading Experience</h4> + + <label> + Number of Stories Shown: <output>10</output> + <input + type="range" + value="10" + min="1" + max="30" + oninput="this.previousElementSibling.value = this.value" + /> + </label> + + <label> + Story Open Mode + <details class="dropdown"> + <summary>Multiple stories</summary> + <ul> + <li>Multiple stories</li> + <li>One story at a time</li> + </ul> + </details> + </label> + + <label> + Story Expand Mode + <details class="dropdown"> + <summary>Double-click to expand all</summary> + <ul> + <li>Always expand all</li> + <li>Double-click to expand all</li> + <li>Never expand all</li> + </ul> + </details> + </label> + + <h4>Navigation</h4> + + <label> + Use Latest URLs + <details class="dropdown"> + <summary>Disabled</summary> + <ul> + <li>Disabled</li> + <li>Enabled</li> + </ul> + </details> + </label> + + <h4>About</h4> + + <label> + Maps Provider + <details class="dropdown"> + <summary>Auto</summary> + <ul> + <li>Auto</li> + <li>Kagi Maps</li> + <li>Google Maps</li> + <li>OpenStreetMap</li> + <li>Apple Maps</li> + </ul> + </details> + </label> + {% else if tab == "sections" %} + <hgroup> + <h4>Article Sections</h4> + <h6>Drag to reorder sections. Use the switch to enable/disable.</h6> + </hgroup> + <article> + <nav> + <ul> + <li>Summary</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Primary Image</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Sources</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Highlights</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Quotes</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Secondary Image</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Perspectives</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Historical Background</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Humanitarian Impact</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Technical Details</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Business Angle</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Scientific Significance</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li>Travel Advisory</li> + </ul> + <ul> + <li> + <label><input name="summary" type="checkbox" role="switch" /></label> + </li> + </ul> + </nav> + </article> + <nav> + <ul></ul> + <ul> + <li> + <button class="secondary">Reset Order</button> + <button>Toggle All</button> + </li> + </ul> + <ul></ul> + </nav> + {% else if tab == "content-filter" %} + <hgroup> + <h4>Filter Presets</h4> + <p>Select one or more preset filters to quickly hide common topics</p> + </hgroup> + <select aria-label="Topics" multiple size="6"> + <option>Cheese</option> + <option selected>Fruits</option> + <option selected>Nuts</option> + <option>Chocolate</option> + <option>Crackers</option> + </select> + <hgroup> + <h4>Custom Keywords</h4> + <p>Add your own keywords to filter, separated by commas</p> + </hgroup> + <input + type="text" + name="custom-keywords" + placeholder="e.g., celebrity name, topic" + aria-label="Custom Keywords" + /> + <hgroup> + <h4>Filter Mode</h4> + <p>Choose how filtered content is handled</p> + </hgroup> + <fieldset> + <label> + <input type="radio" name="language" checked /> + Hide completely + </label> + <label> + <input type="radio" name="language" /> + Blur with warning + </label> + </fieldset> + <hgroup> + <h4>Filter Scope</h4> + <p>Choose which parts of stories to check for keywords</p> + </hgroup> + <select name="filter-scope" required> + <option>Title only</option> + <option>Title and summary</option> + <option selected>All content</option> + </select> + <nav> + <ul> + <li> + <hgroup> + <h4>Show Filtered Count</h4> + <p>Display number of filtered stories in each category</p> + </hgroup> + </li> + </ul> + <ul> + <li> + <input name="terms" type="checkbox" role="switch" /> + </li> + </ul> + </nav> + <hr /> + <hgroup> + <h4>Backup & Restore</h4> + <p> + Export your filter settings to a file or import from a previous backup + </p> + </hgroup> + <button class="secondary">Export Settings</button> + <button>Import Settings</button> + <hr /> + <button class="outline">Reset Content Filter Settings to Defaults</button> + {% else if tab == "syncing" %} + <p> + Kagi News can sync your settings and read history across all your devices. + This data is stored securely on Kagi servers and associated with your Kagi + Search account. Your synced data is not used for any other purpose, not + shared with anyone, and is solely stored to provide the sync service to you. + You have full control over what gets synced and can delete your data at any + time. + </p> + <h4>Sync Preferences</h4> + <label> + <input + name="sync-settings" + type="checkbox" + role="switch" + aria-describedby="sync-settings-description" + /> + Sync Settings + </label> + <label> + <input + name="sync-read-history" + type="checkbox" + role="switch" + aria-describedby="email-helper" + /> + Sync Read History + </label> + <br /> + <h4>Data Management</h4> + <p> + You are in control of your data. Export or delete your cloud data at any + time. Deletion does not affect local data. + </p> + <button class="secondary">Export Data</button> + <button>Clear All Synced Data</button> + {% else if tab == "experimental" %} + <p> + ⚠️ These are experimental features that may be changed or removed at any + time. + </p> + <article> + <nav> + <ul> + <li> + <hgroup> + <h4>Show icons next to articles</h4> + <p> + When enabled, small icons will be displayed next to articles to + provide visual context. + </p> + </hgroup> + </li> + </ul> + <ul> + <li> + <input + name="show-icons-next-to-articles" + type="checkbox" + role="switch" + /> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li> + <hgroup> + <h4>Show icons in category labels</h4> + <p> + When enabled, small icons will be displayed next to category + labels to provide visual context. + </p> + </hgroup> + </li> + </ul> + <ul> + <li> + <input + name="show-icons-next-to-category-labels" + type="checkbox" + role="switch" + /> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li> + <hgroup> + <h4>Disable horizontal category swiping</h4> + <p> + When enabled, horizontal swiping to change categories on mobile + devices will be disabled. + </p> + </hgroup> + </li> + </ul> + <ul> + <li> + <input + name="disable-horizontal-category-swiping" + type="checkbox" + role="switch" + /> + </li> + </ul> + </nav> + </article> + <article> + <nav> + <ul> + <li> + <hgroup> + <h4>Show World Tension Index</h4> + <p> + Display a global temperature reading of world stability based on + current events. + </p> + </hgroup> + </li> + </ul> + <ul> + <li> + <input + name="show-icons-next-to-articles" + type="checkbox" + role="switch" + /> + </li> + </ul> + </nav> + </article> + {% else %} + <p>Select a tab to view settings.</p> + {% endif %} +</section> diff --git a/tests/locustfile.py b/tests/locustfile.py @@ -1,7 +0,0 @@ -from locust import HttpUser, task - -class QuickstartUser(HttpUser): - @task - def home_page(self): - self.client.get("/") - diff --git a/tests/test_simple_web_app/conftest.py b/tests/test_simple_web_app/conftest.py @@ -1,729 +0,0 @@ -import os -import unittest.mock - -import pytest - - -@pytest.fixture() -def test_env() -> dict[str, str]: - return { - "DEBUG": "true", - "DATABASE_PATH": ":memory:", - } - - -import typing -import anyio -import httpx -import math -import io -import contextlib -from urllib.parse import unquote, urljoin -from anyio.streams.stapled import StapledObjectStream -from starlette.types import ASGIApp, Message, Receive, Scope, Send -from starlette.testclient import ASGI3App, _RequestData, _is_asgi3 - - -class AsyncWebSocketTestSession: - def __init__( - self, - app: ASGI3App, - scope: Scope, - ) -> None: - self.app = app - self.scope = scope - self.accepted_subprotocol = None - self.extra_headers = None - - async def __aenter__(self) -> "AsyncWebSocketTestSession": - async with contextlib.AsyncExitStack() as stack: - task_group = await stack.enter_async_context(anyio.create_task_group()) - self.done = anyio.Event() - - async def run( - *, task_status: anyio.abc.TaskStatus[anyio.CancelScope] - ) -> None: - await self._run(task_status=task_status) - self.done.set() - - await task_group.start(run) - stack.push_async_callback(self.done.wait) - stack.callback(task_group.cancel_scope.cancel) - await self.send({"type": "websocket.connect"}) - message = await self.receive() - await self._raise_on_close(message) - self.accepted_subprotocol = message.get("subprotocol", None) - self.extra_headers = message.get("headers", None) - stack.push_async_callback(self.aclose, 1000) - self.exit_stack = stack.pop_all() - return self - - async def __aexit__(self, *args: typing.Any) -> bool | None: - return await self.exit_stack.__aexit__(*args) - - async def _run( - self, *, task_status: anyio.abc.TaskStatus[anyio.CancelScope] - ) -> None: - send: anyio.create_memory_object_stream[Message] = ( - anyio.create_memory_object_stream(math.inf) - ) - send_tx, send_rx = send - receive: anyio.create_memory_object_stream[Message] = ( - anyio.create_memory_object_stream(math.inf) - ) - receive_tx, receive_rx = receive - with send_tx, send_rx, receive_tx, receive_rx, anyio.CancelScope() as cs: - self._receive_tx = receive_tx - self._send_rx = send_rx - task_status.started(cs) - await self.app(self.scope, receive_rx.receive, send_tx.send) - - # wait for cs.cancel to be called before closing streams - await anyio.sleep_forever() - - async def _raise_on_close(self, message: Message) -> None: - if message["type"] == "websocket.close": - raise WebSocketDisconnect( - code=message.get("code", 1000), reason=message.get("reason", "") - ) - elif message["type"] == "websocket.http.response.start": - status_code: int = message["status"] - headers: list[tuple[bytes, bytes]] = message["headers"] - body: list[bytes] = [] - while True: - message = await self.receive() - assert message["type"] == "websocket.http.response.body" - body.append(message["body"]) - if not message.get("more_body", False): - break - raise WebSocketDenialResponse( - status_code=status_code, headers=headers, content=b"".join(body) - ) - - async def send(self, message: Message) -> None: - await self._receive_tx.send(message) - - async def send_text(self, data: str) -> None: - await self.send({"type": "websocket.receive", "text": data}) - - async def send_bytes(self, data: bytes) -> None: - await self.send({"type": "websocket.receive", "bytes": data}) - - async def send_json( - self, data: typing.Any, mode: typing.Literal["text", "binary"] = "text" - ) -> None: - text = json.dumps(data, separators=(",", ":"), ensure_ascii=False) - if mode == "text": - await self.send({"type": "websocket.receive", "text": text}) - else: - await self.send( - {"type": "websocket.receive", "bytes": text.encode("utf-8")} - ) - - async def aclose(self, code: int = 1000, reason: str | None = None) -> None: - await self.send( - {"type": "websocket.disconnect", "code": code, "reason": reason} - ) - - async def receive(self) -> Message: - return await self._send_rx.receive() - - async def receive_text(self) -> str: - message = await self.receive() - await self._raise_on_close(message) - return typing.cast(str, message["text"]) - - async def receive_bytes(self) -> bytes: - message = await self.receive() - await self._raise_on_close(message) - return typing.cast(bytes, message["bytes"]) - - async def receive_json( - self, mode: typing.Literal["text", "binary"] = "text" - ) -> typing.Any: - message = await self.receive() - await self._raise_on_close(message) - if mode == "text": - text = message["text"] - else: - text = message["bytes"].decode("utf-8") - return json.loads(text) - - -class _AsyncTestClientTransport(httpx.AsyncBaseTransport): - def __init__( - self, - app: ASGI3App, - raise_server_exceptions: bool = True, - root_path: str = "", - *, - client: tuple[str, int], - app_state: dict[str, typing.Any], - ) -> None: - self.app = app - self.raise_server_exceptions = raise_server_exceptions - self.root_path = root_path - self.app_state = app_state - self.client = client - - async def handle_async_request(self, request: httpx.Request) -> httpx.Response: - scheme = request.url.scheme - netloc = request.url.netloc.decode(encoding="ascii") - path = request.url.path - raw_path = request.url.raw_path - query = request.url.query.decode(encoding="ascii") - - default_port = {"http": 80, "ws": 80, "https": 443, "wss": 443}[scheme] - - if ":" in netloc: - host, port_string = netloc.split(":", 1) - port = int(port_string) - else: - host = netloc - port = default_port - - # Include the 'host' header. - if "host" in request.headers: - headers: list[tuple[bytes, bytes]] = [] - elif port == default_port: # pragma: no cover - headers = [(b"host", host.encode())] - else: # pragma: no cover - headers = [(b"host", (f"{host}:{port}").encode())] - - # Include other request headers. - headers += [ - (key.lower().encode(), value.encode()) - for key, value in request.headers.multi_items() - ] - - scope: dict[str, typing.Any] - - if scheme in {"ws", "wss"}: - subprotocol = request.headers.get("sec-websocket-protocol", None) - if subprotocol is None: - subprotocols: typing.Sequence[str] = [] - else: - subprotocols = [value.strip() for value in subprotocol.split(",")] - scope = { - "type": "websocket", - "path": unquote(path), - "raw_path": raw_path.split(b"?", 1)[0], - "root_path": self.root_path, - "scheme": scheme, - "query_string": query.encode(), - "headers": headers, - "client": self.client, - "server": [host, port], - "subprotocols": subprotocols, - "state": self.app_state.copy(), - "extensions": {"websocket.http.response": {}}, - } - session = AsyncWebSocketTestSession(self.app, scope) - raise _AsyncUpgrade(session) - - scope = { - "type": "http", - "http_version": "1.1", - "method": request.method, - "path": unquote(path), - "raw_path": raw_path.split(b"?", 1)[0], - "root_path": self.root_path, - "scheme": scheme, - "query_string": query.encode(), - "headers": headers, - "client": self.client, - "server": [host, port], - "extensions": {"http.response.debug": {}}, - "state": self.app_state.copy(), - } - - request_complete = False - response_started = False - response_complete: anyio.Event - raw_kwargs: dict[str, typing.Any] = {"stream": io.BytesIO()} - template = None - context = None - - async def receive() -> Message: - nonlocal request_complete - - if request_complete: - if not response_complete.is_set(): - await response_complete.wait() - return {"type": "http.disconnect"} - - body = request.read() - if isinstance(body, str): - body_bytes: bytes = body.encode("utf-8") # pragma: no cover - elif body is None: - body_bytes = b"" # pragma: no cover - elif isinstance(body, GeneratorType): - try: # pragma: no cover - chunk = body.send(None) - if isinstance(chunk, str): - chunk = chunk.encode("utf-8") - return {"type": "http.request", "body": chunk, "more_body": True} - except StopIteration: # pragma: no cover - request_complete = True - return {"type": "http.request", "body": b""} - else: - body_bytes = body - - request_complete = True - return {"type": "http.request", "body": body_bytes} - - async def send(message: Message) -> None: - nonlocal raw_kwargs, response_started, template, context - - if message["type"] == "http.response.start": - assert not response_started, ( - 'Received multiple "http.response.start" messages.' - ) - raw_kwargs["status_code"] = message["status"] - raw_kwargs["headers"] = [ - (key.decode(), value.decode()) - for key, value in message.get("headers", []) - ] - response_started = True - elif message["type"] == "http.response.body": - assert response_started, ( - 'Received "http.response.body" without "http.response.start".' - ) - assert not response_complete.is_set(), ( - 'Received "http.response.body" after response completed.' - ) - body = message.get("body", b"") - more_body = message.get("more_body", False) - if request.method != "HEAD": - raw_kwargs["stream"].write(body) - if not more_body: - raw_kwargs["stream"].seek(0) - response_complete.set() - elif message["type"] == "http.response.debug": - template = message["info"]["template"] - context = message["info"]["context"] - - try: - response_complete = anyio.Event() - await self.app(scope, receive, send) - except BaseException as exc: - if self.raise_server_exceptions: - raise exc - - if self.raise_server_exceptions: - assert response_started, "TestClient did not receive any response." - elif not response_started: - raw_kwargs = { - "status_code": 500, - "headers": [], - "stream": io.BytesIO(), - } - - raw_kwargs["stream"] = httpx.ByteStream(raw_kwargs["stream"].read()) - - response = httpx.Response(**raw_kwargs, request=request) - if template is not None: - response.template = template # type: ignore[attr-defined] - response.context = context # type: ignore[attr-defined] - return response - - -class _AsyncUpgrade(Exception): - def __init__(self, session: AsyncWebSocketTestSession) -> None: - self.session = session - - -class AsyncTestClient(httpx.AsyncClient): - __test__ = False - - def __init__( - self, - app: ASGIApp, - base_url: str = "http://testserver", - raise_server_exceptions: bool = True, - root_path: str = "", - cookies: httpx._types.CookieTypes | None = None, - headers: dict[str, str] | None = None, - follow_redirects: bool = True, - client: tuple[str, int] = ("testclient", 50000), - ) -> None: - if _is_asgi3(app): - asgi_app = app - else: - app = typing.cast(ASGI2App, app) # type: ignore[assignment] - asgi_app = _WrapASGI2(app) # type: ignore[arg-type] - self.app = asgi_app - self.app_state: dict[str, typing.Any] = {} - transport = _AsyncTestClientTransport( - self.app, - raise_server_exceptions=raise_server_exceptions, - root_path=root_path, - app_state=self.app_state, - client=client, - ) - if headers is None: - headers = {} - headers.setdefault("user-agent", "testclient") - super().__init__( - base_url=base_url, - headers=headers, - transport=transport, - follow_redirects=follow_redirects, - cookies=cookies, - ) - - async def request( # type: ignore[override] - self, - method: str, - url: httpx._types.URLTypes, - *, - content: httpx._types.RequestContent | None = None, - data: _RequestData | None = None, - files: httpx._types.RequestFiles | None = None, - json: typing.Any = None, - params: httpx._types.QueryParamTypes | None = None, - headers: httpx._types.HeaderTypes | None = None, - cookies: httpx._types.CookieTypes | None = None, - auth: httpx._types.AuthTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict[str, typing.Any] | None = None, - ) -> httpx.Response: - if timeout is not httpx.USE_CLIENT_DEFAULT: - warnings.warn( - "You should not use the 'timeout' argument with the TestClient. " - "See https://github.com/encode/starlette/issues/1108 for more information.", - DeprecationWarning, - ) - url = self._merge_url(url) - return await super().request( - method, - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - auth=auth, - follow_redirects=follow_redirects, - timeout=timeout, - extensions=extensions, - ) - - async def get( # type: ignore[override] - self, - url: httpx._types.URLTypes, - *, - params: httpx._types.QueryParamTypes | None = None, - headers: httpx._types.HeaderTypes | None = None, - cookies: httpx._types.CookieTypes | None = None, - auth: httpx._types.AuthTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict[str, typing.Any] | None = None, - ) -> httpx.Response: - return await super().get( - url, - params=params, - headers=headers, - cookies=cookies, - auth=auth, - follow_redirects=follow_redirects, - timeout=timeout, - extensions=extensions, - ) - - async def options( # type: ignore[override] - self, - url: httpx._types.URLTypes, - *, - params: httpx._types.QueryParamTypes | None = None, - headers: httpx._types.HeaderTypes | None = None, - cookies: httpx._types.CookieTypes | None = None, - auth: httpx._types.AuthTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict[str, typing.Any] | None = None, - ) -> httpx.Response: - return await super().options( - url, - params=params, - headers=headers, - cookies=cookies, - auth=auth, - follow_redirects=follow_redirects, - timeout=timeout, - extensions=extensions, - ) - - async def head( # type: ignore[override] - self, - url: httpx._types.URLTypes, - *, - params: httpx._types.QueryParamTypes | None = None, - headers: httpx._types.HeaderTypes | None = None, - cookies: httpx._types.CookieTypes | None = None, - auth: httpx._types.AuthTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict[str, typing.Any] | None = None, - ) -> httpx.Response: - return await super().head( - url, - params=params, - headers=headers, - cookies=cookies, - auth=auth, - follow_redirects=follow_redirects, - timeout=timeout, - extensions=extensions, - ) - - async def post( # type: ignore[override] - self, - url: httpx._types.URLTypes, - *, - content: httpx._types.RequestContent | None = None, - data: _RequestData | None = None, - files: httpx._types.RequestFiles | None = None, - json: typing.Any = None, - params: httpx._types.QueryParamTypes | None = None, - headers: httpx._types.HeaderTypes | None = None, - cookies: httpx._types.CookieTypes | None = None, - auth: httpx._types.AuthTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict[str, typing.Any] | None = None, - ) -> httpx.Response: - return await super().post( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - auth=auth, - follow_redirects=follow_redirects, - timeout=timeout, - extensions=extensions, - ) - - async def put( # type: ignore[override] - self, - url: httpx._types.URLTypes, - *, - content: httpx._types.RequestContent | None = None, - data: _RequestData | None = None, - files: httpx._types.RequestFiles | None = None, - json: typing.Any = None, - params: httpx._types.QueryParamTypes | None = None, - headers: httpx._types.HeaderTypes | None = None, - cookies: httpx._types.CookieTypes | None = None, - auth: httpx._types.AuthTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict[str, typing.Any] | None = None, - ) -> httpx.Response: - return await super().put( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - auth=auth, - follow_redirects=follow_redirects, - timeout=timeout, - extensions=extensions, - ) - - async def patch( # type: ignore[override] - self, - url: httpx._types.URLTypes, - *, - content: httpx._types.RequestContent | None = None, - data: _RequestData | None = None, - files: httpx._types.RequestFiles | None = None, - json: typing.Any = None, - params: httpx._types.QueryParamTypes | None = None, - headers: httpx._types.HeaderTypes | None = None, - cookies: httpx._types.CookieTypes | None = None, - auth: httpx._types.AuthTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict[str, typing.Any] | None = None, - ) -> httpx.Response: - return await super().patch( - url, - content=content, - data=data, - files=files, - json=json, - params=params, - headers=headers, - cookies=cookies, - auth=auth, - follow_redirects=follow_redirects, - timeout=timeout, - extensions=extensions, - ) - - async def delete( # type: ignore[override] - self, - url: httpx._types.URLTypes, - *, - params: httpx._types.QueryParamTypes | None = None, - headers: httpx._types.HeaderTypes | None = None, - cookies: httpx._types.CookieTypes | None = None, - auth: httpx._types.AuthTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - follow_redirects: bool - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - timeout: httpx._types.TimeoutTypes - | httpx._client.UseClientDefault = httpx._client.USE_CLIENT_DEFAULT, - extensions: dict[str, typing.Any] | None = None, - ) -> httpx.Response: - return await super().delete( - url, - params=params, - headers=headers, - cookies=cookies, - auth=auth, - follow_redirects=follow_redirects, - timeout=timeout, - extensions=extensions, - ) - - async def websocket_connect( - self, - url: str, - subprotocols: typing.Sequence[str] | None = None, - **kwargs: typing.Any, - ) -> AsyncWebSocketTestSession: - url = urljoin("ws://testserver", url) - headers = kwargs.get("headers", {}) - headers.setdefault("connection", "upgrade") - headers.setdefault("sec-websocket-key", "testserver==") - headers.setdefault("sec-websocket-version", "13") - if subprotocols is not None: - headers.setdefault("sec-websocket-protocol", ", ".join(subprotocols)) - kwargs["headers"] = headers - try: - await super().request("GET", url, **kwargs) - except _AsyncUpgrade as exc: - session = exc.session - else: - raise RuntimeError("Expected WebSocket upgrade") # pragma: no cover - - return session - - async def __aenter__(self) -> "AsyncTestClient": - async with contextlib.AsyncExitStack() as stack: - task_group = await stack.enter_async_context(anyio.create_task_group()) - send: anyio.create_memory_object_stream[ - typing.MutableMapping[str, typing.Any] | None - ] = anyio.create_memory_object_stream(math.inf) - receive: anyio.create_memory_object_stream[ - typing.MutableMapping[str, typing.Any] - ] = anyio.create_memory_object_stream(math.inf) - for channel in (*send, *receive): - stack.push_async_callback(channel.aclose) - self.stream_send = StapledObjectStream(*send) - self.stream_receive = StapledObjectStream(*receive) - self.task_done = anyio.Event() - - async def lifespan() -> None: - await self.lifespan() - self.task_done.set() - - task_group.start_soon(lifespan) - await self.wait_startup() - - @stack.push_async_callback - async def wait_shutdown() -> None: - await self.wait_shutdown() - - self.exit_stack = stack.pop_all() - - return self - - async def __aexit__(self, *args: typing.Any) -> None: - await self.exit_stack.aclose() - - async def lifespan(self) -> None: - scope = {"type": "lifespan", "state": self.app_state} - try: - await self.app(scope, self.stream_receive.receive, self.stream_send.send) - finally: - try: - await self.stream_send.send(None) - except anyio.ClosedResourceError: - pass - - async def wait_startup(self) -> None: - await self.stream_receive.send({"type": "lifespan.startup"}) - - async def receive() -> typing.Any: - message = await self.stream_send.receive() - if message is None: - await self.task_done.wait() - return message - - message = await receive() - assert message["type"] in ( - "lifespan.startup.complete", - "lifespan.startup.failed", - ) - if message["type"] == "lifespan.startup.failed": - await receive() - - async def wait_shutdown(self) -> None: - async def receive() -> typing.Any: - message = await self.stream_send.receive() - if message is None: - await self.task_done.wait() - return message - - await self.stream_receive.send({"type": "lifespan.shutdown"}) - message = await receive() - assert message["type"] in ( - "lifespan.shutdown.complete", - "lifespan.shutdown.failed", - ) - if message["type"] == "lifespan.shutdown.failed": - await receive() - - -@pytest.fixture() -async def async_test_client(test_env): - with unittest.mock.patch.dict(os.environ, test_env, clear=True): - from simple_web_app.app import app - - return AsyncTestClient(app) diff --git a/tests/test_simple_web_app/test_app.py b/tests/test_simple_web_app/test_app.py @@ -1,170 +0,0 @@ -import datetime -import collections - -import aiosqlite -import pytest -from bs4 import BeautifulSoup - - -DummyData = collections.namedtuple("DummyData", ["table", "cols", "rows"]) - - -async def insert_dummy_data(conn: aiosqlite.Connection, data: DummyData): - await conn.executemany(f"INSERT INTO {data.table} ({', '.join(data.cols)}) VALUES ({', '.join(['?'] * len(data.cols))});", data.rows) - - -@pytest.fixture() -def test_news_item() -> DummyData: - return DummyData( - table="news_item", - cols=["id", "url", "title", "text", "published", "author", "language"], - rows=[ - (1, "https://example.com", "Test Title", "Asdf.", datetime.datetime(2025, 8, 1, 20, 42, 35, 123, tzinfo=datetime.UTC).isoformat(), "Silas", "english"), - (2, "https://example.com", "Test Title", "Asdf.", datetime.datetime(2025, 8, 1, 20, 42, 35, 123, tzinfo=datetime.UTC).isoformat(), "Silas", "english"), - (3, "https://example.com", "Test Title", "Asdf.", datetime.datetime(2025, 8, 1, 20, 42, 35, 123, tzinfo=datetime.UTC).isoformat(), "Silas", "english"), - (4, "https://example.com", "Test Title", "Asdf.", datetime.datetime(2025, 8, 1, 20, 42, 35, 123, tzinfo=datetime.UTC).isoformat(), "Silas", "english"), - (5, "https://example.com", "Test Title", "Asdf.", datetime.datetime(2025, 8, 1, 20, 42, 35, 123, tzinfo=datetime.UTC).isoformat(), "Silas", "english"), - (6, "https://example.com", "Test Title", "Asdf.", datetime.datetime(2025, 8, 1, 20, 42, 35, 123, tzinfo=datetime.UTC).isoformat(), "Silas", "english"), - ] - ) - - -@pytest.fixture() -def test_category() -> DummyData: - return DummyData( - table="category", - cols=["id", "name"], - rows=[(1, "Category 1")] - ) - - -@pytest.fixture() -def test_news_item_category() -> DummyData: - return DummyData( - table="news_item_category", - cols=["id", "news_item_id", "category_id"], - rows=[ - (1, 1, 1), - (2, 2, 1), - (3, 3, 1), - (4, 4, 1), - (5, 5, 1), - (6, 6, 1), - ] - ) - - -@pytest.fixture() -def test_data(test_news_item, test_category, test_news_item_category) -> tuple[DummyData, ...]: - return (test_news_item, test_category, test_news_item_category) - - -async def test_home_page(async_test_client, test_data): - """Full home page render.""" - async with async_test_client as client: - conn = client.app_state["conn"] - for d in test_data: - await insert_dummy_data(conn, d) - response = await client.get("/") - - assert response.status_code == 200 - assert response.template.name == "application.html" - assert len(response.context["news"]) > 0 - - -async def test_home_htmx(async_test_client, test_data): - """Partial home template render for HTMX.""" - async with async_test_client as client: - conn = client.app_state["conn"] - for d in test_data: - await insert_dummy_data(conn, d) - response = await client.get("/", headers={"HX-Request": "true"}) - - assert response.status_code == 200 - assert response.template.name == "oob_swap.html" - - -async def test_select_chain(async_test_client, test_data): - """""" - async with async_test_client as client: - conn = client.app_state["conn"] - for d in test_data: - await insert_dummy_data(conn, d) - - # Open the home page - response = await client.get("/") - assert response.status_code == 200 - assert response.template.name == "application.html" - assert len(response.context["news"]) > 0 - soup = BeautifulSoup(response.text, features="html.parser") - load_more_button = soup.find("button", {"id": "load-more-btn"}) - assert "page=1" in load_more_button.attrs["hx-get"] - assert "category_id" not in load_more_button.attrs["hx-get"] - - # Load more data - path = load_more_button["hx-get"] - response = await client.get(path, headers={"HX-Request": "true"}) - assert response.status_code == 200 - assert len(response.context["news"]) > 0 - - # Filter for category 2, which doesn't exist - # TODO: should this instead throw an error? - response = await client.get("/?category_id=2", headers={"HX-Request": "true"}) - assert response.template.name == "oob_swap.html" - assert len(response.context["news"]) == 0 - assert response.context["category_id"] == 2 - soup = BeautifulSoup(response.text, features="html.parser") - load_more_button = soup.find("button", {"id": "load-more-btn"}) - assert "page=1" in load_more_button.attrs["hx-get"] - assert "category_id=2" in load_more_button.attrs["hx-get"] - - # Filter for category 1, show only news with that category - response = await client.get("/?category_id=1", headers={"HX-Request": "true"}) - assert response.template.name == "oob_swap.html" - assert len(response.context["news"]) > 0 - assert response.context["category_id"] == 1 - assert all(any(c["id"] == 1 for c in n["categories"]) for n in response.context["news"]) # Every row contains the category we filtered by - soup = BeautifulSoup(response.text, features="html.parser") - load_more_button = soup.find("button", {"id": "load-more-btn"}) - assert "disabled" not in load_more_button.attrs - assert "page=1" in load_more_button.attrs["hx-get"] - assert "category_id=1" in load_more_button.attrs["hx-get"] - - # Load more news, should all still be category one; we only have 1 left so the load more button should be disabled - path = load_more_button["hx-get"] - response = await client.get(path, headers={"HX-Request": "true"}) - assert response.template.name == "oob_swap.html" - assert len(response.context["news"]) > 0 - assert response.context["category_id"] == 1 - assert all(any(c["id"] == 1 for c in n["categories"]) for n in response.context["news"]) # Every row contains the category we filtered by - soup = BeautifulSoup(response.text, features="html.parser") - load_more_button = soup.find("button", {"id": "load-more-btn"}) - assert "disabled" in load_more_button.attrs - assert "page=2" in load_more_button.attrs["hx-get"] - assert "category_id=1" in load_more_button.attrs["hx-get"] - - -async def test_settings(async_test_client, test_data): - async with async_test_client as client: - # Open the home page - response = await client.get("/") - soup = BeautifulSoup(response.text, features="html.parser") - modal_placeholder = soup.find("div", {"id": "modal-placeholder"}) - assert len(list(modal_placeholder.children)) == 0 - - # Open the settings modal - settings_button = soup.find("a", {"id": "settings-btn"}) - path = settings_button["hx-get"] - response = await client.get(path, headers={"HX-Request": "true"}) - assert response.template.name == "settings.html" - soup = BeautifulSoup(response.text, features="html.parser") - assert soup.find("h4").text == "Appearance" - # TODO: check that the modal_placeholder has an element - - # Open the sync tab within the settings - sync_tab = soup.find("div", {"id": "syncing-tab"}) - path = sync_tab["hx-get"] - response = await client.get(path, headers={"HX-Request": "true"}) - assert response.template.name == "settings_tab.html" - soup = BeautifulSoup(response.text, features="html.parser") - assert soup.find("h4").text == "Sync Preferences" diff --git a/uv.lock b/uv.lock @@ -1,1043 +0,0 @@ -version = 1 -revision = 3 -requires-python = "==3.13.*" - -[[package]] -name = "aiosql" -version = "13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/3a/105cdf480d444ee059f3fbea65616fba006fba29d32e382fb1a4d947f4b7/aiosql-13.4.tar.gz", hash = "sha256:da6ebb4d5e735753853007a7d574e60761338fc6ca8d95d6d95b5d85d7b1354d", size = 75730, upload-time = "2025-04-09T08:27:36.593Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/cf/219d8fdae6b6ea9192111b68b611550852f0b082ace9f23e391dde639f28/aiosql-13.4-py3-none-any.whl", hash = "sha256:f5dc4a93484a96ee9008f8488cc1be100e1ebf08cd2135ccade89975332bf49f", size = 24783, upload-time = "2025-04-09T08:27:34.753Z" }, -] - -[[package]] -name = "aiosqlite" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/7d/8bca2bf9a247c2c5dfeec1d7a5f40db6518f88d314b8bca9da29670d2671/aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3", size = 13454, upload-time = "2025-02-03T07:30:16.235Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, -] - -[[package]] -name = "anyio" -version = "4.11.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, -] - -[[package]] -name = "argon2-cffi" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "argon2-cffi-bindings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, -] - -[[package]] -name = "argon2-cffi-bindings" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, - { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, - { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, - { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, - { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, - { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, - { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, - { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, -] - -[[package]] -name = "beautifulsoup4" -version = "4.14.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" }, -] - -[[package]] -name = "bidict" -version = "0.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, -] - -[[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 = "brotli" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload-time = "2023-09-07T14:05:41.643Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681, upload-time = "2024-10-18T12:32:34.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475, upload-time = "2024-10-18T12:32:36.485Z" }, - { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173, upload-time = "2024-10-18T12:32:37.978Z" }, - { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803, upload-time = "2024-10-18T12:32:39.606Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946, upload-time = "2024-10-18T12:32:41.679Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707, upload-time = "2024-10-18T12:32:43.478Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231, upload-time = "2024-10-18T12:32:45.224Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157, upload-time = "2024-10-18T12:32:46.894Z" }, - { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122, upload-time = "2024-10-18T12:32:48.844Z" }, - { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206, upload-time = "2024-10-18T12:32:51.198Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804, upload-time = "2024-10-18T12:32:52.661Z" }, - { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517, upload-time = "2024-10-18T12:32:54.066Z" }, -] - -[[package]] -name = "certifi" -version = "2025.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, -] - -[[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" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, -] - -[[package]] -name = "click" -version = "8.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, -] - -[[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 = "configargparse" -version = "1.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size = 43958, upload-time = "2025-05-23T14:26:17.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" }, -] - -[[package]] -name = "coverage" -version = "7.10.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, - { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, - { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, - { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, - { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, - { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, - { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, - { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, - { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, - { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, - { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, - { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, - { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, - { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, - { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, - { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, - { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, - { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, - { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, - { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, - { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, -] - -[[package]] -name = "flask" -version = "3.1.2" -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/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, -] - -[[package]] -name = "flask-cors" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/37/bcfa6c7d5eec777c4c7cf45ce6b27631cebe5230caf88d85eadd63edd37a/flask_cors-6.0.1.tar.gz", hash = "sha256:d81bcb31f07b0985be7f48406247e9243aced229b7747219160a0559edd678db", size = 13463, upload-time = "2025-06-11T01:32:08.518Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/f8/01bf35a3afd734345528f98d0353f2a978a476528ad4d7e78b70c4d149dd/flask_cors-6.0.1-py3-none-any.whl", hash = "sha256:c7b2cbfb1a31aa0d2e5341eea03a6805349f7a61647daee1a15c46bbe981494c", size = 13244, upload-time = "2025-06-11T01:32:07.352Z" }, -] - -[[package]] -name = "flask-login" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/6e/2f4e13e373bb49e68c02c51ceadd22d172715a06716f9299d9df01b6ddb2/Flask-Login-0.6.3.tar.gz", hash = "sha256:5e23d14a607ef12806c699590b89d0f0e0d67baeec599d75947bf9c147330333", size = 48834, upload-time = "2023-10-30T14:53:21.151Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303, upload-time = "2023-10-30T14:53:19.636Z" }, -] - -[[package]] -name = "gevent" -version = "25.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, - { name = "greenlet", marker = "platform_python_implementation == 'CPython'" }, - { name = "zope-event" }, - { name = "zope-interface" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f1/58/267e8160aea00ab00acd2de97197eecfe307064a376fb5c892870a8a6159/gevent-25.5.1.tar.gz", hash = "sha256:582c948fa9a23188b890d0bc130734a506d039a2e5ad87dae276a456cc683e61", size = 6388207, upload-time = "2025-05-12T12:57:59.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/25/2162b38d7b48e08865db6772d632bd1648136ce2bb50e340565e45607cad/gevent-25.5.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a022a9de9275ce0b390b7315595454258c525dc8287a03f1a6cacc5878ab7cbc", size = 2928044, upload-time = "2025-05-12T11:11:36.33Z" }, - { url = "https://files.pythonhosted.org/packages/1b/e0/dbd597a964ed00176da122ea759bf2a6c1504f1e9f08e185379f92dc355f/gevent-25.5.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fae8533f9d0ef3348a1f503edcfb531ef7a0236b57da1e24339aceb0ce52922", size = 1788751, upload-time = "2025-05-12T11:52:32.643Z" }, - { url = "https://files.pythonhosted.org/packages/f1/74/960cc4cf4c9c90eafbe0efc238cdf588862e8e278d0b8c0d15a0da4ed480/gevent-25.5.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c7b32d9c3b5294b39ea9060e20c582e49e1ec81edbfeae6cf05f8ad0829cb13d", size = 1869766, upload-time = "2025-05-12T11:54:23.903Z" }, - { url = "https://files.pythonhosted.org/packages/56/78/fa84b1c7db79b156929685db09a7c18c3127361dca18a09e998e98118506/gevent-25.5.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b95815fe44f318ebbfd733b6428b4cb18cc5e68f1c40e8501dd69cc1f42a83d", size = 1835358, upload-time = "2025-05-12T12:00:06.794Z" }, - { url = "https://files.pythonhosted.org/packages/00/5c/bfefe3822bbca5b83bfad256c82251b3f5be13d52d14e17a786847b9b625/gevent-25.5.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d316529b70d325b183b2f3f5cde958911ff7be12eb2b532b5c301f915dbbf1e", size = 2073071, upload-time = "2025-05-12T11:33:04.2Z" }, - { url = "https://files.pythonhosted.org/packages/20/e4/08a77a3839a37db96393dea952e992d5846a881b887986dde62ead6b48a1/gevent-25.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f6ba33c13db91ffdbb489a4f3d177a261ea1843923e1d68a5636c53fe98fa5ce", size = 1809805, upload-time = "2025-05-12T12:00:00.537Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ac/28848348f790c1283df74b0fc0a554271d0606676470f848eccf84eae42a/gevent-25.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ee34b77c7553777c0b8379915f75934c3f9c8cd32f7cd098ea43c9323c2276", size = 2138305, upload-time = "2025-05-12T11:40:56.566Z" }, - { url = "https://files.pythonhosted.org/packages/52/9e/0e9e40facd2d714bfb00f71fc6dacaacc82c24c1c2e097bf6461e00dec9f/gevent-25.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fa6aa0da224ed807d3b76cdb4ee8b54d4d4d5e018aed2478098e685baae7896", size = 1637444, upload-time = "2025-05-12T12:17:45.995Z" }, -] - -[[package]] -name = "geventhttpclient" -version = "2.3.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "brotli" }, - { name = "certifi" }, - { name = "gevent" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/89/19/1ca8de73dcc0596d3df01be299e940d7fc3bccbeb6f62bb8dd2d427a3a50/geventhttpclient-2.3.4.tar.gz", hash = "sha256:1749f75810435a001fc6d4d7526c92cf02b39b30ab6217a886102f941c874222", size = 83545, upload-time = "2025-06-11T13:18:14.144Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/ad/132fddde6e2dca46d6a86316962437acd2bfaeb264db4e0fae83c529eb04/geventhttpclient-2.3.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:be64c5583884c407fc748dedbcb083475d5b138afb23c6bc0836cbad228402cc", size = 71967, upload-time = "2025-06-11T13:17:22.121Z" }, - { url = "https://files.pythonhosted.org/packages/f4/34/5e77d9a31d93409a8519cf573843288565272ae5a016be9c9293f56c50a1/geventhttpclient-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:15b2567137734183efda18e4d6245b18772e648b6a25adea0eba8b3a8b0d17e8", size = 52632, upload-time = "2025-06-11T13:17:23.016Z" }, - { url = "https://files.pythonhosted.org/packages/47/d2/cf0dbc333304700e68cee9347f654b56e8b0f93a341b8b0d027ee96800d6/geventhttpclient-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a4bca1151b8cd207eef6d5cb3c720c562b2aa7293cf113a68874e235cfa19c31", size = 51980, upload-time = "2025-06-11T13:17:23.933Z" }, - { url = "https://files.pythonhosted.org/packages/27/6e/049e685fc43e2e966c83f24b3187f6a6736103f0fc51118140f4ca1793d4/geventhttpclient-2.3.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8a681433e2f3d4b326d8b36b3e05b787b2c6dd2a5660a4a12527622278bf02ed", size = 114998, upload-time = "2025-08-24T12:16:54.72Z" }, - { url = "https://files.pythonhosted.org/packages/24/13/1d08cf0400bf0fe0bb21e70f3f5fab2130aecef962b4362b7a1eba3cd738/geventhttpclient-2.3.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:736aa8e9609e4da40aeff0dbc02fea69021a034f4ed1e99bf93fc2ca83027b64", size = 115690, upload-time = "2025-08-24T12:16:56.328Z" }, - { url = "https://files.pythonhosted.org/packages/fd/bc/15d22882983cac573859d274783c5b0a95881e553fc312e7b646be432668/geventhttpclient-2.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9d477ae1f5d42e1ee6abbe520a2e9c7f369781c3b8ca111d1f5283c1453bc825", size = 121681, upload-time = "2025-08-24T12:16:58.344Z" }, - { url = "https://files.pythonhosted.org/packages/ec/5b/c0c30ccd9d06c603add3f2d6abd68bd98430ee9730dc5478815759cf07f7/geventhttpclient-2.3.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b50d9daded5d36193d67e2fc30e59752262fcbbdc86e8222c7df6b93af0346a", size = 118987, upload-time = "2025-06-11T13:17:24.97Z" }, - { url = "https://files.pythonhosted.org/packages/4f/56/095a46af86476372064128162eccbd2ba4a7721503759890d32ea701d5fd/geventhttpclient-2.3.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe705e7656bc6982a463a4ed7f9b1db8c78c08323f1d45d0d1d77063efa0ce96", size = 124519, upload-time = "2025-06-11T13:17:25.933Z" }, - { url = "https://files.pythonhosted.org/packages/ae/12/7c9ba94b58f7954a83d33183152ce6bf5bda10c08ebe47d79a314cd33e29/geventhttpclient-2.3.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69668589359db4cbb9efa327dda5735d1e74145e6f0a9ffa50236d15cf904053", size = 115574, upload-time = "2025-06-11T13:17:27.331Z" }, - { url = "https://files.pythonhosted.org/packages/73/77/c4e7c5bce0199428fdb811d6adf6e347180d89eaa1b9b723f711f6bbc830/geventhttpclient-2.3.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ba526e07ccaf4f1c2cd3395dda221139f01468b6eee1190d4a616f187a0378", size = 114222, upload-time = "2025-06-11T13:17:28.289Z" }, - { url = "https://files.pythonhosted.org/packages/a3/79/58802d300950dbd7d4e31eb24afd7c270fc7900ff3923fd266cc915bb086/geventhttpclient-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:525bd192705b5cb41a7cc3fe41fca194bfd6b5b59997ab9fe68fe0a82dab6140", size = 111682, upload-time = "2025-06-11T13:17:29.291Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9c/ae04e4033459b8142788dad80d8d0b42d460bc6db9150e0815c2d0a02cb4/geventhttpclient-2.3.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:42b6f6afb0d3aab6a013c9cdb97e19bf4fe08695975670d0a018113d24cb344c", size = 113252, upload-time = "2025-06-11T13:17:30.357Z" }, - { url = "https://files.pythonhosted.org/packages/d3/67/5ae5d5878b06397a7b54334d1d31bb78cefc950ae890c2b8f5c917eb271e/geventhttpclient-2.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:227579b703085c4e5c6d5217ad6565b19ac8d1164404133e5874efaae1905114", size = 118426, upload-time = "2025-06-11T13:17:31.363Z" }, - { url = "https://files.pythonhosted.org/packages/ca/36/9065bb51f261950c42eddf8718e01a9ff344d8082e31317a8b6677be9bd6/geventhttpclient-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d1d0db89c1c8f3282eac9a22fda2b4082e1ed62a2107f70e3f1de1872c7919f", size = 112245, upload-time = "2025-06-11T13:17:32.331Z" }, - { url = "https://files.pythonhosted.org/packages/21/7e/08a615bec095c288f997951e42e48b262d43c6081bef33cfbfad96ab9658/geventhttpclient-2.3.4-cp313-cp313-win32.whl", hash = "sha256:4e492b9ab880f98f8a9cc143b96ea72e860946eae8ad5fb2837cede2a8f45154", size = 48360, upload-time = "2025-06-11T13:17:33.349Z" }, - { url = "https://files.pythonhosted.org/packages/ec/19/ef3cb21e7e95b14cfcd21e3ba7fe3d696e171682dfa43ab8c0a727cac601/geventhttpclient-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:72575c5b502bf26ececccb905e4e028bb922f542946be701923e726acf305eb6", size = 48956, upload-time = "2025-06-11T13:17:34.956Z" }, -] - -[[package]] -name = "greenlet" -version = "3.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, - { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, - { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, - { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, - { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, - { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, - { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, - { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httptools" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214, upload-time = "2024-10-16T19:44:38.738Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431, upload-time = "2024-10-16T19:44:39.818Z" }, - { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121, upload-time = "2024-10-16T19:44:41.189Z" }, - { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805, upload-time = "2024-10-16T19:44:42.384Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858, upload-time = "2024-10-16T19:44:43.959Z" }, - { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042, upload-time = "2024-10-16T19:44:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682, upload-time = "2024-10-16T19:44:46.46Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[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 = "locust" -version = "2.41.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "configargparse" }, - { name = "flask" }, - { name = "flask-cors" }, - { name = "flask-login" }, - { name = "gevent" }, - { name = "geventhttpclient" }, - { name = "locust-cloud" }, - { name = "msgpack" }, - { name = "psutil" }, - { name = "pytest" }, - { name = "python-engineio" }, - { name = "python-socketio", extra = ["client"] }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "pyzmq" }, - { name = "requests" }, - { name = "setuptools" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/97/9e176b560152f97454aeb908ef61619ae2d8df8ad8cf2ec3829b9a29c8ca/locust-2.41.3.tar.gz", hash = "sha256:531804b6900472bad7f1123c73b81720826d502254b754f70907240d99663491", size = 1415538, upload-time = "2025-10-01T13:33:52.446Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/55/bfc3df125d1f4b798ae9abe33b5e2917f7c4711ef2f40940c2c71eaedacf/locust-2.41.3-py3-none-any.whl", hash = "sha256:dd55ac0eee5cd5f7f478eea20adfc83b7cdbead71532a746faa355dff525e5ae", size = 1434071, upload-time = "2025-10-01T13:33:51.176Z" }, -] - -[[package]] -name = "locust-cloud" -version = "1.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "configargparse" }, - { name = "gevent" }, - { name = "platformdirs" }, - { name = "python-engineio" }, - { name = "python-socketio", extra = ["client"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/63/330c4a7a1d114c0687d1ad58326ad27fa128208f0166eb000331a5e9b72f/locust_cloud-1.27.2.tar.gz", hash = "sha256:0d80d63ba722a45c39dcdce66ca5a389944db2123a5fff59d1438e4ee3701258", size = 454487, upload-time = "2025-10-01T12:26:45.752Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/ef/49ecd09f0717494f1303b2889a4cdb4b2984465cf6c738812e9f23170b93/locust_cloud-1.27.2-py3-none-any.whl", hash = "sha256:cd21ee33e5ea721c0e4f3c0e691e9c637fc88783d234b060ae1693c26010604e", size = 409313, upload-time = "2025-10-01T12:26:43.682Z" }, -] - -[[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" }, -] - -[[package]] -name = "msgpack" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555, upload-time = "2025-06-13T06:52:51.324Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677, upload-time = "2025-06-13T06:52:16.64Z" }, - { url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603, upload-time = "2025-06-13T06:52:17.843Z" }, - { url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504, upload-time = "2025-06-13T06:52:18.982Z" }, - { url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a", size = 423749, upload-time = "2025-06-13T06:52:20.211Z" }, - { url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac", size = 404458, upload-time = "2025-06-13T06:52:21.429Z" }, - { url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b", size = 405976, upload-time = "2025-06-13T06:52:22.995Z" }, - { url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7", size = 408607, upload-time = "2025-06-13T06:52:24.152Z" }, - { url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172, upload-time = "2025-06-13T06:52:25.704Z" }, - { url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323", size = 65347, upload-time = "2025-06-13T06:52:26.846Z" }, - { url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69", size = 72341, upload-time = "2025-06-13T06:52:27.835Z" }, -] - -[[package]] -name = "orjson" -version = "3.11.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", size = 5482394, upload-time = "2025-08-26T17:46:43.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", size = 238127, upload-time = "2025-08-26T17:45:38.146Z" }, - { url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", size = 127494, upload-time = "2025-08-26T17:45:39.57Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", size = 123017, upload-time = "2025-08-26T17:45:40.876Z" }, - { url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", size = 127898, upload-time = "2025-08-26T17:45:42.188Z" }, - { url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c", size = 130742, upload-time = "2025-08-26T17:45:43.511Z" }, - { url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", size = 132377, upload-time = "2025-08-26T17:45:45.525Z" }, - { url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", size = 135313, upload-time = "2025-08-26T17:45:46.821Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", size = 132908, upload-time = "2025-08-26T17:45:48.126Z" }, - { url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", size = 130905, upload-time = "2025-08-26T17:45:49.414Z" }, - { url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", size = 403812, upload-time = "2025-08-26T17:45:51.085Z" }, - { url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", size = 146277, upload-time = "2025-08-26T17:45:52.851Z" }, - { url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", size = 135418, upload-time = "2025-08-26T17:45:54.806Z" }, - { url = "https://files.pythonhosted.org/packages/60/d4/bae8e4f26afb2c23bea69d2f6d566132584d1c3a5fe89ee8c17b718cab67/orjson-3.11.3-cp313-cp313-win32.whl", hash = "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b", size = 136216, upload-time = "2025-08-26T17:45:57.182Z" }, - { url = "https://files.pythonhosted.org/packages/88/76/224985d9f127e121c8cad882cea55f0ebe39f97925de040b75ccd4b33999/orjson-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae", size = 131362, upload-time = "2025-08-26T17:45:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cf/0dce7a0be94bd36d1346be5067ed65ded6adb795fdbe3abd234c8d576d01/orjson-3.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce", size = 125989, upload-time = "2025-08-26T17:45:59.95Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "psutil" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/31/4723d756b59344b643542936e37a31d1d3204bcdc42a7daa8ee9eb06fb50/psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2", size = 497660, upload-time = "2025-09-17T20:14:52.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/62/ce4051019ee20ce0ed74432dd73a5bb087a6704284a470bb8adff69a0932/psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13", size = 245242, upload-time = "2025-09-17T20:14:56.126Z" }, - { url = "https://files.pythonhosted.org/packages/38/61/f76959fba841bf5b61123fbf4b650886dc4094c6858008b5bf73d9057216/psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5", size = 246682, upload-time = "2025-09-17T20:14:58.25Z" }, - { url = "https://files.pythonhosted.org/packages/88/7a/37c99d2e77ec30d63398ffa6a660450b8a62517cabe44b3e9bae97696e8d/psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3", size = 287994, upload-time = "2025-09-17T20:14:59.901Z" }, - { url = "https://files.pythonhosted.org/packages/9d/de/04c8c61232f7244aa0a4b9a9fbd63a89d5aeaf94b2fc9d1d16e2faa5cbb0/psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3", size = 291163, upload-time = "2025-09-17T20:15:01.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/58/c4f976234bf6d4737bc8c02a81192f045c307b72cf39c9e5c5a2d78927f6/psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d", size = 293625, upload-time = "2025-09-17T20:15:04.492Z" }, - { url = "https://files.pythonhosted.org/packages/79/87/157c8e7959ec39ced1b11cc93c730c4fb7f9d408569a6c59dbd92ceb35db/psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca", size = 244812, upload-time = "2025-09-17T20:15:07.462Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e9/b44c4f697276a7a95b8e94d0e320a7bf7f3318521b23de69035540b39838/psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d", size = 247965, upload-time = "2025-09-17T20:15:09.673Z" }, - { url = "https://files.pythonhosted.org/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07", size = 244971, upload-time = "2025-09-17T20:15:12.262Z" }, -] - -[[package]] -name = "pycparser" -version = "2.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, -] - -[[package]] -name = "pytest-asyncio" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, -] - -[[package]] -name = "pytest-cov" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage" }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, -] - -[[package]] -name = "python-engineio" -version = "4.12.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "simple-websocket" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/d8/63e5535ab21dc4998ba1cfe13690ccf122883a38f025dca24d6e56c05eba/python_engineio-4.12.3.tar.gz", hash = "sha256:35633e55ec30915e7fc8f7e34ca8d73ee0c080cec8a8cd04faf2d7396f0a7a7a", size = 91910, upload-time = "2025-09-28T06:31:36.765Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/f0/c5aa0a69fd9326f013110653543f36ece4913c17921f3e1dbd78e1b423ee/python_engineio-4.12.3-py3-none-any.whl", hash = "sha256:7c099abb2a27ea7ab429c04da86ab2d82698cdd6c52406cb73766fe454feb7e1", size = 59637, upload-time = "2025-09-28T06:31:35.354Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, -] - -[[package]] -name = "python-socketio" -version = "5.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bidict" }, - { name = "python-engineio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/1a/396d50ccf06ee539fa758ce5623b59a9cb27637fc4b2dc07ed08bf495e77/python_socketio-5.13.0.tar.gz", hash = "sha256:ac4e19a0302ae812e23b712ec8b6427ca0521f7c582d6abb096e36e24a263029", size = 121125, upload-time = "2025-04-12T15:46:59.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/32/b4fb8585d1be0f68bde7e110dffbcf354915f77ad8c778563f0ad9655c02/python_socketio-5.13.0-py3-none-any.whl", hash = "sha256:51f68d6499f2df8524668c24bcec13ba1414117cfb3a90115c559b601ab10caf", size = 77800, upload-time = "2025-04-12T15:46:58.412Z" }, -] - -[package.optional-dependencies] -client = [ - { name = "requests" }, - { name = "websocket-client" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, -] - -[[package]] -name = "pyzmq" -version = "27.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, - { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, - { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, - { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, - { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, - { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - -[[package]] -name = "ruff" -version = "0.13.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/8e/f9f9ca747fea8e3ac954e3690d4698c9737c23b51731d02df999c150b1c9/ruff-0.13.3.tar.gz", hash = "sha256:5b0ba0db740eefdfbcce4299f49e9eaefc643d4d007749d77d047c2bab19908e", size = 5438533, upload-time = "2025-10-02T19:29:31.582Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/33/8f7163553481466a92656d35dea9331095122bb84cf98210bef597dd2ecd/ruff-0.13.3-py3-none-linux_armv6l.whl", hash = "sha256:311860a4c5e19189c89d035638f500c1e191d283d0cc2f1600c8c80d6dcd430c", size = 12484040, upload-time = "2025-10-02T19:28:49.199Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b5/4a21a4922e5dd6845e91896b0d9ef493574cbe061ef7d00a73c61db531af/ruff-0.13.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2bdad6512fb666b40fcadb65e33add2b040fc18a24997d2e47fee7d66f7fcae2", size = 13122975, upload-time = "2025-10-02T19:28:52.446Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/15649af836d88c9f154e5be87e64ae7d2b1baa5a3ef317cb0c8fafcd882d/ruff-0.13.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fc6fa4637284708d6ed4e5e970d52fc3b76a557d7b4e85a53013d9d201d93286", size = 12346621, upload-time = "2025-10-02T19:28:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/bcbccb8141305f9a6d3f72549dd82d1134299177cc7eaf832599700f95a7/ruff-0.13.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c9e6469864f94a98f412f20ea143d547e4c652f45e44f369d7b74ee78185838", size = 12574408, upload-time = "2025-10-02T19:28:56.679Z" }, - { url = "https://files.pythonhosted.org/packages/ce/19/0f3681c941cdcfa2d110ce4515624c07a964dc315d3100d889fcad3bfc9e/ruff-0.13.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5bf62b705f319476c78891e0e97e965b21db468b3c999086de8ffb0d40fd2822", size = 12285330, upload-time = "2025-10-02T19:28:58.79Z" }, - { url = "https://files.pythonhosted.org/packages/10/f8/387976bf00d126b907bbd7725219257feea58650e6b055b29b224d8cb731/ruff-0.13.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78cc1abed87ce40cb07ee0667ce99dbc766c9f519eabfd948ed87295d8737c60", size = 13980815, upload-time = "2025-10-02T19:29:01.577Z" }, - { url = "https://files.pythonhosted.org/packages/0c/a6/7c8ec09d62d5a406e2b17d159e4817b63c945a8b9188a771193b7e1cc0b5/ruff-0.13.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4fb75e7c402d504f7a9a259e0442b96403fa4a7310ffe3588d11d7e170d2b1e3", size = 14987733, upload-time = "2025-10-02T19:29:04.036Z" }, - { url = "https://files.pythonhosted.org/packages/97/e5/f403a60a12258e0fd0c2195341cfa170726f254c788673495d86ab5a9a9d/ruff-0.13.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b951f9d9afb39330b2bdd2dd144ce1c1335881c277837ac1b50bfd99985ed3", size = 14439848, upload-time = "2025-10-02T19:29:06.684Z" }, - { url = "https://files.pythonhosted.org/packages/39/49/3de381343e89364c2334c9f3268b0349dc734fc18b2d99a302d0935c8345/ruff-0.13.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6052f8088728898e0a449f0dde8fafc7ed47e4d878168b211977e3e7e854f662", size = 13421890, upload-time = "2025-10-02T19:29:08.767Z" }, - { url = "https://files.pythonhosted.org/packages/ab/b5/c0feca27d45ae74185a6bacc399f5d8920ab82df2d732a17213fb86a2c4c/ruff-0.13.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc742c50f4ba72ce2a3be362bd359aef7d0d302bf7637a6f942eaa763bd292af", size = 13444870, upload-time = "2025-10-02T19:29:11.234Z" }, - { url = "https://files.pythonhosted.org/packages/50/a1/b655298a1f3fda4fdc7340c3f671a4b260b009068fbeb3e4e151e9e3e1bf/ruff-0.13.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8e5640349493b378431637019366bbd73c927e515c9c1babfea3e932f5e68e1d", size = 13691599, upload-time = "2025-10-02T19:29:13.353Z" }, - { url = "https://files.pythonhosted.org/packages/32/b0/a8705065b2dafae007bcae21354e6e2e832e03eb077bb6c8e523c2becb92/ruff-0.13.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b139f638a80eae7073c691a5dd8d581e0ba319540be97c343d60fb12949c8d0", size = 12421893, upload-time = "2025-10-02T19:29:15.668Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/cbe7082588d025cddbb2f23e6dfef08b1a2ef6d6f8328584ad3015b5cebd/ruff-0.13.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6b547def0a40054825de7cfa341039ebdfa51f3d4bfa6a0772940ed351d2746c", size = 12267220, upload-time = "2025-10-02T19:29:17.583Z" }, - { url = "https://files.pythonhosted.org/packages/a5/99/4086f9c43f85e0755996d09bdcb334b6fee9b1eabdf34e7d8b877fadf964/ruff-0.13.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9cc48a3564423915c93573f1981d57d101e617839bef38504f85f3677b3a0a3e", size = 13177818, upload-time = "2025-10-02T19:29:19.943Z" }, - { url = "https://files.pythonhosted.org/packages/9b/de/7b5db7e39947d9dc1c5f9f17b838ad6e680527d45288eeb568e860467010/ruff-0.13.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a993b17ec03719c502881cb2d5f91771e8742f2ca6de740034433a97c561989", size = 13618715, upload-time = "2025-10-02T19:29:22.527Z" }, - { url = "https://files.pythonhosted.org/packages/28/d3/bb25ee567ce2f61ac52430cf99f446b0e6d49bdfa4188699ad005fdd16aa/ruff-0.13.3-py3-none-win32.whl", hash = "sha256:f14e0d1fe6460f07814d03c6e32e815bff411505178a1f539a38f6097d3e8ee3", size = 12334488, upload-time = "2025-10-02T19:29:24.782Z" }, - { url = "https://files.pythonhosted.org/packages/cf/49/12f5955818a1139eed288753479ba9d996f6ea0b101784bb1fe6977ec128/ruff-0.13.3-py3-none-win_amd64.whl", hash = "sha256:621e2e5812b691d4f244638d693e640f188bacbb9bc793ddd46837cea0503dd2", size = 13455262, upload-time = "2025-10-02T19:29:26.882Z" }, - { url = "https://files.pythonhosted.org/packages/fe/72/7b83242b26627a00e3af70d0394d68f8f02750d642567af12983031777fc/ruff-0.13.3-py3-none-win_arm64.whl", hash = "sha256:9e9e9d699841eaf4c2c798fa783df2fabc680b72059a02ca0ed81c460bc58330", size = 12538484, upload-time = "2025-10-02T19:29:28.951Z" }, -] - -[[package]] -name = "setuptools" -version = "80.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, -] - -[[package]] -name = "simple-web-app" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "aiosql" }, - { name = "aiosqlite" }, - { name = "argon2-cffi" }, - { name = "httptools" }, - { name = "jinja2" }, - { name = "orjson" }, - { name = "python-multipart" }, - { name = "starlette" }, - { name = "uvicorn" }, - { name = "uvloop" }, -] - -[package.dev-dependencies] -dev = [ - { name = "beautifulsoup4" }, - { name = "httpx" }, - { name = "locust" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "ruff" }, - { name = "watchfiles" }, -] -test = [ - { name = "beautifulsoup4" }, - { name = "httpx" }, - { name = "locust" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, -] - -[package.metadata] -requires-dist = [ - { name = "aiosql" }, - { name = "aiosqlite" }, - { name = "argon2-cffi" }, - { name = "httptools" }, - { name = "jinja2" }, - { name = "orjson" }, - { name = "python-multipart" }, - { name = "starlette" }, - { name = "uvicorn" }, - { name = "uvloop" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "beautifulsoup4" }, - { name = "httpx" }, - { name = "locust" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "ruff" }, - { name = "watchfiles" }, -] -test = [ - { name = "beautifulsoup4" }, - { name = "httpx" }, - { name = "locust" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, -] - -[[package]] -name = "simple-websocket" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wsproto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "soupsieve" -version = "2.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, -] - -[[package]] -name = "starlette" -version = "0.48.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, -] - -[[package]] -name = "uvloop" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123, upload-time = "2024-10-14T23:38:00.688Z" }, - { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325, upload-time = "2024-10-14T23:38:02.309Z" }, - { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806, upload-time = "2024-10-14T23:38:04.711Z" }, - { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068, upload-time = "2024-10-14T23:38:06.385Z" }, - { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428, upload-time = "2024-10-14T23:38:08.416Z" }, - { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018, upload-time = "2024-10-14T23:38:10.888Z" }, -] - -[[package]] -name = "watchfiles" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/9a/d451fcc97d029f5812e898fd30a53fd8c15c7bbd058fd75cfc6beb9bd761/watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575", size = 94406, upload-time = "2025-06-15T19:06:59.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/42/fae874df96595556a9089ade83be34a2e04f0f11eb53a8dbf8a8a5e562b4/watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30", size = 402004, upload-time = "2025-06-15T19:05:38.499Z" }, - { url = "https://files.pythonhosted.org/packages/fa/55/a77e533e59c3003d9803c09c44c3651224067cbe7fb5d574ddbaa31e11ca/watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a", size = 393671, upload-time = "2025-06-15T19:05:39.52Z" }, - { url = "https://files.pythonhosted.org/packages/05/68/b0afb3f79c8e832e6571022611adbdc36e35a44e14f129ba09709aa4bb7a/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc", size = 449772, upload-time = "2025-06-15T19:05:40.897Z" }, - { url = "https://files.pythonhosted.org/packages/ff/05/46dd1f6879bc40e1e74c6c39a1b9ab9e790bf1f5a2fe6c08b463d9a807f4/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b", size = 456789, upload-time = "2025-06-15T19:05:42.045Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ca/0eeb2c06227ca7f12e50a47a3679df0cd1ba487ea19cf844a905920f8e95/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895", size = 482551, upload-time = "2025-06-15T19:05:43.781Z" }, - { url = "https://files.pythonhosted.org/packages/31/47/2cecbd8694095647406645f822781008cc524320466ea393f55fe70eed3b/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a", size = 597420, upload-time = "2025-06-15T19:05:45.244Z" }, - { url = "https://files.pythonhosted.org/packages/d9/7e/82abc4240e0806846548559d70f0b1a6dfdca75c1b4f9fa62b504ae9b083/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b", size = 477950, upload-time = "2025-06-15T19:05:46.332Z" }, - { url = "https://files.pythonhosted.org/packages/25/0d/4d564798a49bf5482a4fa9416dea6b6c0733a3b5700cb8a5a503c4b15853/watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c", size = 451706, upload-time = "2025-06-15T19:05:47.459Z" }, - { url = "https://files.pythonhosted.org/packages/81/b5/5516cf46b033192d544102ea07c65b6f770f10ed1d0a6d388f5d3874f6e4/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b", size = 625814, upload-time = "2025-06-15T19:05:48.654Z" }, - { url = "https://files.pythonhosted.org/packages/0c/dd/7c1331f902f30669ac3e754680b6edb9a0dd06dea5438e61128111fadd2c/watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb", size = 622820, upload-time = "2025-06-15T19:05:50.088Z" }, - { url = "https://files.pythonhosted.org/packages/1b/14/36d7a8e27cd128d7b1009e7715a7c02f6c131be9d4ce1e5c3b73d0e342d8/watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9", size = 279194, upload-time = "2025-06-15T19:05:51.186Z" }, - { url = "https://files.pythonhosted.org/packages/25/41/2dd88054b849aa546dbeef5696019c58f8e0774f4d1c42123273304cdb2e/watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7", size = 292349, upload-time = "2025-06-15T19:05:52.201Z" }, - { url = "https://files.pythonhosted.org/packages/c8/cf/421d659de88285eb13941cf11a81f875c176f76a6d99342599be88e08d03/watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5", size = 283836, upload-time = "2025-06-15T19:05:53.265Z" }, - { url = "https://files.pythonhosted.org/packages/45/10/6faf6858d527e3599cc50ec9fcae73590fbddc1420bd4fdccfebffeedbc6/watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1", size = 400343, upload-time = "2025-06-15T19:05:54.252Z" }, - { url = "https://files.pythonhosted.org/packages/03/20/5cb7d3966f5e8c718006d0e97dfe379a82f16fecd3caa7810f634412047a/watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339", size = 392916, upload-time = "2025-06-15T19:05:55.264Z" }, - { url = "https://files.pythonhosted.org/packages/8c/07/d8f1176328fa9e9581b6f120b017e286d2a2d22ae3f554efd9515c8e1b49/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633", size = 449582, upload-time = "2025-06-15T19:05:56.317Z" }, - { url = "https://files.pythonhosted.org/packages/66/e8/80a14a453cf6038e81d072a86c05276692a1826471fef91df7537dba8b46/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011", size = 456752, upload-time = "2025-06-15T19:05:57.359Z" }, - { url = "https://files.pythonhosted.org/packages/5a/25/0853b3fe0e3c2f5af9ea60eb2e781eade939760239a72c2d38fc4cc335f6/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670", size = 481436, upload-time = "2025-06-15T19:05:58.447Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9e/4af0056c258b861fbb29dcb36258de1e2b857be4a9509e6298abcf31e5c9/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf", size = 596016, upload-time = "2025-06-15T19:05:59.59Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fa/95d604b58aa375e781daf350897aaaa089cff59d84147e9ccff2447c8294/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4", size = 476727, upload-time = "2025-06-15T19:06:01.086Z" }, - { url = "https://files.pythonhosted.org/packages/65/95/fe479b2664f19be4cf5ceeb21be05afd491d95f142e72d26a42f41b7c4f8/watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20", size = 451864, upload-time = "2025-06-15T19:06:02.144Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8a/3c4af14b93a15ce55901cd7a92e1a4701910f1768c78fb30f61d2b79785b/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef", size = 625626, upload-time = "2025-06-15T19:06:03.578Z" }, - { url = "https://files.pythonhosted.org/packages/da/f5/cf6aa047d4d9e128f4b7cde615236a915673775ef171ff85971d698f3c2c/watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb", size = 622744, upload-time = "2025-06-15T19:06:05.066Z" }, -] - -[[package]] -name = "websocket-client" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, -] - -[[package]] -name = "werkzeug" -version = "3.1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, -] - -[[package]] -name = "wsproto" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" }, -] - -[[package]] -name = "zope-event" -version = "6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/d8/9c8b0c6bb1db09725395618f68d3b8a08089fca0aed28437500caaf713ee/zope_event-6.0.tar.gz", hash = "sha256:0ebac894fa7c5f8b7a89141c272133d8c1de6ddc75ea4b1f327f00d1f890df92", size = 18731, upload-time = "2025-09-12T07:10:13.551Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/b5/1abb5a8b443314c978617bf46d5d9ad648bdf21058074e817d7efbb257db/zope_event-6.0-py3-none-any.whl", hash = "sha256:6f0922593407cc673e7d8766b492c519f91bdc99f3080fe43dcec0a800d682a3", size = 6409, upload-time = "2025-09-12T07:10:12.316Z" }, -] - -[[package]] -name = "zope-interface" -version = "8.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/3a/7fcf02178b8fad0a51e67e32765cd039ae505d054d744d76b8c2bbcba5ba/zope_interface-8.0.1.tar.gz", hash = "sha256:eba5610d042c3704a48222f7f7c6ab5b243ed26f917e2bc69379456b115e02d1", size = 253746, upload-time = "2025-09-25T05:55:51.285Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/dc/3c12fca01c910c793d636ffe9c0984e0646abaf804e44552070228ed0ede/zope_interface-8.0.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:c7cc027fc5c61c5d69e5080c30b66382f454f43dc379c463a38e78a9c6bab71a", size = 208992, upload-time = "2025-09-25T05:58:40.712Z" }, - { url = "https://files.pythonhosted.org/packages/46/71/6127b7282a3e380ca927ab2b40778a9c97935a4a57a2656dadc312db5f30/zope_interface-8.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fcf9097ff3003b7662299f1c25145e15260ec2a27f9a9e69461a585d79ca8552", size = 209051, upload-time = "2025-09-25T05:58:42.182Z" }, - { url = "https://files.pythonhosted.org/packages/56/86/4387a9f951ee18b0e41fda77da77d59c33e59f04660578e2bad688703e64/zope_interface-8.0.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6d965347dd1fb9e9a53aa852d4ded46b41ca670d517fd54e733a6b6a4d0561c2", size = 259223, upload-time = "2025-09-25T05:58:23.191Z" }, - { url = "https://files.pythonhosted.org/packages/61/08/ce60a114466abc067c68ed41e2550c655f551468ae17b4b17ea360090146/zope_interface-8.0.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a3b8bb77a4b89427a87d1e9eb969ab05e38e6b4a338a9de10f6df23c33ec3c2", size = 264690, upload-time = "2025-09-25T05:58:15.052Z" }, - { url = "https://files.pythonhosted.org/packages/36/9a/62a9ba3a919594605a07c34eee3068659bbd648e2fa0c4a86d876810b674/zope_interface-8.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:87e6b089002c43231fb9afec89268391bcc7a3b66e76e269ffde19a8112fb8d5", size = 264201, upload-time = "2025-09-25T06:26:27.797Z" }, - { url = "https://files.pythonhosted.org/packages/da/06/8fe88bd7edef60566d21ef5caca1034e10f6b87441ea85de4bbf9ea74768/zope_interface-8.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:64a43f5280aa770cbafd0307cb3d1ff430e2a1001774e8ceb40787abe4bb6658", size = 212273, upload-time = "2025-09-25T06:00:25.398Z" }, -]