From c04ad071694e9fe479da962bfd0f9139aaa8f133 Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Sun, 15 Jun 2025 13:24:17 +0200 Subject: [PATCH] Initial commit --- .env.example | 8 + .gitignore | 3 + Containerfile | 71 ++ README.md | 7 + entrypoint.sh | 4 + poetry.lock | 667 ++++++++++++++++++ pyproject.toml | 28 + src/invidious_export_server/__init__.py | 59 ++ src/invidious_export_server/authenticator.py | 47 ++ .../blueprints/__init__.py | 11 + .../blueprints/auth/__init__.py | 44 ++ .../blueprints/auth/templates/login.html | 19 + .../blueprints/control/__init__.py | 88 +++ .../control/templates/data_control.html | 35 + .../control/templates/data_control_guest.html | 32 + src/invidious_export_server/export.py | 40 ++ .../static/favicon.webp | Bin 0 -> 808 bytes src/invidious_export_server/static/style.css | 60 ++ .../templates/index.html | 15 + .../templates/layout.html | 48 ++ 20 files changed, 1286 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Containerfile create mode 100644 README.md create mode 100644 entrypoint.sh create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 src/invidious_export_server/__init__.py create mode 100644 src/invidious_export_server/authenticator.py create mode 100644 src/invidious_export_server/blueprints/__init__.py create mode 100644 src/invidious_export_server/blueprints/auth/__init__.py create mode 100644 src/invidious_export_server/blueprints/auth/templates/login.html create mode 100644 src/invidious_export_server/blueprints/control/__init__.py create mode 100644 src/invidious_export_server/blueprints/control/templates/data_control.html create mode 100644 src/invidious_export_server/blueprints/control/templates/data_control_guest.html create mode 100644 src/invidious_export_server/export.py create mode 100644 src/invidious_export_server/static/favicon.webp create mode 100644 src/invidious_export_server/static/style.css create mode 100644 src/invidious_export_server/templates/index.html create mode 100644 src/invidious_export_server/templates/layout.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8133371 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Must be PostgreSQL +DATABASE_URL=postgres://kemal:kemal@[fc6c:b484:25b1:d25d::1]/invidious + +# Generate using `openssl rand -hex 16` +QUART_SECRET_KEY=0359e1c42e06fbaa284567ef7dc0faae + +# Any Invidious instance +ALTERNATIVE_INSTANCE_URL=https://redirect.invidious.io \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c32e91 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ + +.env \ No newline at end of file diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..aa6e9d1 --- /dev/null +++ b/Containerfile @@ -0,0 +1,71 @@ +# ===================================================================== +# Builder Stage +# +# This stage installs dependencies and builds the virtual environment. +# ===================================================================== +FROM python:3.13-slim as builder + +# Set environment variables to prevent writing .pyc files and for unbuffered output +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +WORKDIR /app + +# Install build-time system dependencies and Poetry +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +RUN pip install poetry + +# Configure Poetry to create the virtual env in /.venv +# This makes it easy to copy to the next stage +ENV POETRY_VIRTUALENVS_IN_PROJECT=true + +# Copy dependency files and install them +COPY pyproject.toml poetry.lock ./ +# The virtual env will be created in /app/.venv +RUN poetry install --only main --sync --no-root + +# Copy the application code +COPY src/ ./src/ + + +# ===================================================================== +# Final Stage +# +# This stage creates the final, minimal image for production. +# ===================================================================== +FROM python:3.13-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Set the working directory +WORKDIR /app + +# Create a non-root user and group +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Copy the virtual environment from the builder stage +COPY --from=builder --chown=appuser:appuser /app/.venv ./.venv + +# Copy the application code from the builder stage +COPY --from=builder --chown=appuser:appuser /app/src ./src + +# Copy entrypoint +COPY --chown=appuser:appuser entrypoint.sh ./ +RUN chmod +x ./entrypoint.sh + +ENV PATH="/app/.venv/bin:$PATH" + +# Make port 8000 available +EXPOSE 8000 + +# Switch to the non-root user +USER appuser + +# Set the entrypoint script +ENTRYPOINT ["./entrypoint.sh"] + +# Define the default command to run the application +# This is passed as arguments to the entrypoint script +CMD ["granian", "--interface", "asgi", "src.invidious_export_server:app"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c98246 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +### Environment variables +- `QUART_SECRET_KEY` +- `DATABASE_URL` +- `ALTERNATIVE_INSTANCE_URL`, defaults to `https://redirect.invidious.io` + +### Health check +A `/health` endpoint is provided that returns `OK` \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..1004f47 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -e + +exec "$@" \ No newline at end of file diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..9a7b1a7 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,667 @@ +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. + +[[package]] +name = "aiofiles" +version = "24.1.0" +description = "File support for asyncio." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] + +[[package]] +name = "asyncpg" +version = "0.30.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "asyncpg-0.30.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfb4dd5ae0699bad2b233672c8fc5ccbd9ad24b89afded02341786887e37927e"}, + {file = "asyncpg-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc1f62c792752a49f88b7e6f774c26077091b44caceb1983509edc18a2222ec0"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3152fef2e265c9c24eec4ee3d22b4f4d2703d30614b0b6753e9ed4115c8a146f"}, + {file = "asyncpg-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7255812ac85099a0e1ffb81b10dc477b9973345793776b128a23e60148dd1af"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:578445f09f45d1ad7abddbff2a3c7f7c291738fdae0abffbeb737d3fc3ab8b75"}, + {file = "asyncpg-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c42f6bb65a277ce4d93f3fba46b91a265631c8df7250592dd4f11f8b0152150f"}, + {file = "asyncpg-0.30.0-cp310-cp310-win32.whl", hash = "sha256:aa403147d3e07a267ada2ae34dfc9324e67ccc4cdca35261c8c22792ba2b10cf"}, + {file = "asyncpg-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb622c94db4e13137c4c7f98834185049cc50ee01d8f657ef898b6407c7b9c50"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a"}, + {file = "asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a"}, + {file = "asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056"}, + {file = "asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454"}, + {file = "asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d"}, + {file = "asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e"}, + {file = "asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3"}, + {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a"}, + {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af"}, + {file = "asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e"}, + {file = "asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70"}, + {file = "asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33"}, + {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4"}, + {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba"}, + {file = "asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590"}, + {file = "asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:29ff1fc8b5bf724273782ff8b4f57b0f8220a1b2324184846b39d1ab4122031d"}, + {file = "asyncpg-0.30.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64e899bce0600871b55368b8483e5e3e7f1860c9482e7f12e0a771e747988168"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b290f4726a887f75dcd1b3006f484252db37602313f806e9ffc4e5996cfe5cb"}, + {file = "asyncpg-0.30.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f86b0e2cd3f1249d6fe6fd6cfe0cd4538ba994e2d8249c0491925629b9104d0f"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:393af4e3214c8fa4c7b86da6364384c0d1b3298d45803375572f415b6f673f38"}, + {file = "asyncpg-0.30.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fd4406d09208d5b4a14db9a9dbb311b6d7aeeab57bded7ed2f8ea41aeef39b34"}, + {file = "asyncpg-0.30.0-cp38-cp38-win32.whl", hash = "sha256:0b448f0150e1c3b96cb0438a0d0aa4871f1472e58de14a3ec320dbb2798fb0d4"}, + {file = "asyncpg-0.30.0-cp38-cp38-win_amd64.whl", hash = "sha256:f23b836dd90bea21104f69547923a02b167d999ce053f3d502081acea2fba15b"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f4e83f067b35ab5e6371f8a4c93296e0439857b4569850b178a01385e82e9ad"}, + {file = "asyncpg-0.30.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5df69d55add4efcd25ea2a3b02025b669a285b767bfbf06e356d68dbce4234ff"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3479a0d9a852c7c84e822c073622baca862d1217b10a02dd57ee4a7a081f708"}, + {file = "asyncpg-0.30.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26683d3b9a62836fad771a18ecf4659a30f348a561279d6227dab96182f46144"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1b982daf2441a0ed314bd10817f1606f1c28b1136abd9e4f11335358c2c631cb"}, + {file = "asyncpg-0.30.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c06a3a50d014b303e5f6fc1e5f95eb28d2cee89cf58384b700da621e5d5e547"}, + {file = "asyncpg-0.30.0-cp39-cp39-win32.whl", hash = "sha256:1b11a555a198b08f5c4baa8f8231c74a366d190755aa4f99aacec5970afe929a"}, + {file = "asyncpg-0.30.0-cp39-cp39-win_amd64.whl", hash = "sha256:8b684a3c858a83cd876f05958823b68e8d14ec01bb0c0d14a6704c5bf9711773"}, + {file = "asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851"}, +] + +[package.extras] +docs = ["Sphinx (>=8.1.3,<8.2.0)", "sphinx-rtd-theme (>=1.2.2)"] +gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""] +test = ["distro (>=1.9.0,<1.10.0)", "flake8 (>=6.1,<7.0)", "flake8-pyi (>=24.1.0,<24.2.0)", "gssapi ; platform_system == \"Linux\"", "k5test ; platform_system == \"Linux\"", "mypy (>=1.8.0,<1.9.0)", "sspilib ; platform_system == \"Windows\"", "uvloop (>=0.15.3) ; platform_system != \"Windows\" and python_version < \"3.14.0\""] + +[[package]] +name = "bcrypt" +version = "4.3.0" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"}, + {file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"}, + {file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"}, + {file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"}, + {file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"}, + {file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"}, + {file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"}, + {file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "buildpg" +version = "0.4" +description = "Query building for the postgresql prepared statements and asyncpg." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "buildpg-0.4-py3-none-any.whl", hash = "sha256:20d539976c81ea6f5529d3930016b0482ed0ff06def3d6da79d0fc0a3bbaeeb1"}, + {file = "buildpg-0.4.tar.gz", hash = "sha256:3a6c1f40fb6c826caa819d84727e36a1372f7013ba696637b492e5935916d479"}, +] + +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "flask" +version = "3.1.1" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "flask-3.1.1-py3-none-any.whl", hash = "sha256:07aae2bb5eaf77993ef57e357491839f5fd9f4dc281593a81a9e4d79a24f295c"}, + {file = "flask-3.1.1.tar.gz", hash = "sha256:284c7b8f2f58cb737f0cf1c30fd7eaf0ccfcde196099d24ecede3fc2005aa59e"}, +] + +[package.dependencies] +blinker = ">=1.9.0" +click = ">=8.1.3" +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "granian" +version = "2.3.4" +description = "A Rust HTTP server for Python applications" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "granian-2.3.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f8f8817bffe4d5ae6e1b4b3be87dcabefe7418b85e8d9745ce260a9273e5aa15"}, + {file = "granian-2.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:edc2747b0114e461020d68e26a86db42f1e387432f5d0f6842d29bc4342f7a24"}, + {file = "granian-2.3.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da26987cd231ebd6d67af76ce04d399ff65f571480e642798663f0a49780e415"}, + {file = "granian-2.3.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1972f7c24ac7aefafac2ed7fada54834aa8fa23448b104cfecee05178d4e880c"}, + {file = "granian-2.3.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e645d90d2c641df8be36f697f4df3c6be62711a95499a82751ab350469f12a"}, + {file = "granian-2.3.4-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:264f315b5ac4310ec4750a221ca7c418bde442e43f6d0689185cefc9ec1c40e9"}, + {file = "granian-2.3.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f08064baeb55678840749d42753fab1a450442d56bd17a71e57c9228b8127435"}, + {file = "granian-2.3.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:ee5db8b73f09b9bac03e2cdd1a962e3ea588ee3b2ac22a991fdf9cb2aae319cb"}, + {file = "granian-2.3.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:02123fe5b8615c465821b8dd1cf23b995d539ac3fee02c8423223b580c3531af"}, + {file = "granian-2.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:5bef52a7b13654d38346fc09d368895d2c033438b38585d52786509480142d8c"}, + {file = "granian-2.3.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:1760bf5a6daa18b597f0044266396269fcb2bb952aaff617d055e4ea30ed1bdb"}, + {file = "granian-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5f75b7d360c91c59d32f3040883fa880472efb165b98c37bf5de0306f784eb44"}, + {file = "granian-2.3.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb8d7b4c26a71c2ac9b6c54c5f591775bf2e21698730d71201e321181f217b81"}, + {file = "granian-2.3.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d977f28453af8a6d50bc3edeba5bc85933fd0bb3cf046be41c2bde2789a6736e"}, + {file = "granian-2.3.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c9d1abea6bae50b65bb980c0d3a4cc87ba7d138dd0d34c82d471a07e4582b55"}, + {file = "granian-2.3.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:325c9c767ff2292d479f2145e3d9c6ed8117327ff4a5c6437a4140efef0e8eee"}, + {file = "granian-2.3.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62d9865d8d28ea1c5d5a933d6fcf9dd08de41b2c9bbf8e8e630f3076e1e1d295"}, + {file = "granian-2.3.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:99977564fce60c8c84a84b8ec0de9ce239d5497febbfc3c01ed269b7ba4260dc"}, + {file = "granian-2.3.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e0fd3af360f934bdeb348e4da7d81b3efeb27ed85b4baac4ad335972b81d6644"}, + {file = "granian-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:eed0552f77caa2b11789cf59244f5625b2327689cc36e3de6e3fced428353088"}, + {file = "granian-2.3.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8f2b2c8e0cb60bc7121ba1c92826056d5a787ae7bd0be4033aaea25b0ca244f8"}, + {file = "granian-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b1dc70db92729796d0bf94afe4680500e0264958a90939bc4c8a207f68d83f89"}, + {file = "granian-2.3.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f206da0f25916a538f3c042b980b41e7df00f7eea52df2303887efb76069d52"}, + {file = "granian-2.3.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f2e56ce5aaf54d4412cd903a85d3a210f97fc65f2672b6bd7aa3ee98479fbb8"}, + {file = "granian-2.3.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3491f1989059c3ac8df03a8fdced1233e144c068acddb1b93d6eb0c24fcc38f9"}, + {file = "granian-2.3.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5187a30d86c3bd05939ee6b61ca542a98106e7b9ac4a28c53137db3d6f7a6f84"}, + {file = "granian-2.3.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9dbf756a9861ac9c3788c2d9bff93496a7a287a6bc54bfcce443b8b8d41f4e90"}, + {file = "granian-2.3.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:20ae1c2fe95a88053d8c1face5e072b281b607344b3d8c437c99b7c1bb9ce9be"}, + {file = "granian-2.3.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b0ccfe516a8356437747596d2f94f8db2e1a855463ca299574f4e357154598b4"}, + {file = "granian-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:bac75562ede2c419dc93323a82eee101c6dbc12d2ebed8d856d338e8ff48ce36"}, + {file = "granian-2.3.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f3a7488836f064bb3dedabd09c223bf7408e9fd5bf3a77587c73b51d6ce1dbce"}, + {file = "granian-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6ba37638c0bfa1b68c852738290ffac99f657ffbc2d6cdcc832d311e189d1947"}, + {file = "granian-2.3.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29d6d5d407dbd5fcbe58a412619ca060fa3de26e6f37ef48f0ebef2eb8bbec57"}, + {file = "granian-2.3.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:90d2c23a8df642af2bac8e40d17b09550d67498b2a8ea6a8ff8844edba9c14e8"}, + {file = "granian-2.3.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b94e70080877f368578bc8040936c605eebee204905cd64af0497a4c34307209"}, + {file = "granian-2.3.4-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a1a393fbb8a3245c0b26ae2cf37649d0f1438d61ea780445885212b878ba4e5d"}, + {file = "granian-2.3.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:b390427ac03fd09e6fdf5d290f47480b6e1471e2abf3889d278f119cbea97256"}, + {file = "granian-2.3.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6135af0ed734b718f1d7ac2540a088971b06ef65db9f58d44c1ce7dd5df354ac"}, + {file = "granian-2.3.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:bac27d8b9bbecd5c2027a23cef6135d83ec05f6d0061b40dfdd9587a3f761579"}, + {file = "granian-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:baca4b613eb9545bc309af213afe1940e4253b22445da667c47f841bb7baa666"}, + {file = "granian-2.3.4-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:2224b46ad524d92ab8df5e301c489f2839312b8b487b3dcf8c032ef7f4a9b402"}, + {file = "granian-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:670ce012d4f6fc045698bdf55b8d7ca262876d4c2fb894aceb5e015b1c488c25"}, + {file = "granian-2.3.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fedf6fe1c2a4cf2141860d45ff82861bf257c9d71f39e225f1989091d9656c40"}, + {file = "granian-2.3.4-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:41b020b24ac3888c477a2baf488e39cc64cc1235f6053d4d788542d56acbbb6c"}, + {file = "granian-2.3.4-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:1fb9132a3535f3d399ed89621580b6eaed6c9df5c3f673e0a6205c987abb26d0"}, + {file = "granian-2.3.4-cp313-cp313t-musllinux_1_1_armv7l.whl", hash = "sha256:c223467643ca1e6436804cc84ec828c6f08c5c24beee6c861f76c9d65b5086c1"}, + {file = "granian-2.3.4-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:4b2cef0a948a1278a6c7e5ae1414dbb852f893d56e9574da52e5f89c55fa9c3f"}, + {file = "granian-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:161d7abdc3097669136be543245d687fa23e1c69936b1844f8c5334aadc060ef"}, + {file = "granian-2.3.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:41da9681fe87f2d4d5593ea00743591c76110e90d1ca9e397d99aebcad33a223"}, + {file = "granian-2.3.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91fbf1219436b12d3bb8696b5cd53ebc35d3b3af96eab6876efc47e920da507c"}, + {file = "granian-2.3.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:005db5fce5e4994c05374e84e78ed45a2ed0df5e09cb8b7d0c744e99b89d065d"}, + {file = "granian-2.3.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de3204f9cf98c4a8fe3059faee1bab84e32e155f62b18affc3e5b7a5978aa478"}, + {file = "granian-2.3.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc785a361c3ce618b1294e0351d439405a2447fa6ec0f6733dfcb9294a187c66"}, + {file = "granian-2.3.4-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:efcc903437a0fe9d8defcec0ece2f9a3a74d8d23914542995723e3908b41c281"}, + {file = "granian-2.3.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b2ca9e619619f5ac9087804b95e5d0e4dae18fadf8e2484e54495ea10c221f3d"}, + {file = "granian-2.3.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:fd0d88ec65b05c02de2784a8503a4278813c63f956d205d61feac47fcbea341c"}, + {file = "granian-2.3.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:deb731cea580c417a4df90ecd88d9efb7e9c4f54c3f67af60854248b2790dbae"}, + {file = "granian-2.3.4-cp39-cp39-win_amd64.whl", hash = "sha256:f3ce5d692e85b2755046b3afce91e3603fef6438fc1c4a408ccb5b7041aca127"}, + {file = "granian-2.3.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:415a19c95565cd5b2e8f640053a2bcdaaa892c413f862255d49255bec861659e"}, + {file = "granian-2.3.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:79f9310a6476280c5da451f4c63378376d0aed3278608f3266bdb02dc6227c68"}, + {file = "granian-2.3.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f710698771b9b773350e5e67e9c7bfd2dcc2a6c3c849cd157bf794cabbdae76e"}, + {file = "granian-2.3.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ed6faf7fc90d0314a0e0c199a9834a18dc69462f2de294dee99b0724263df165"}, + {file = "granian-2.3.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4d5222a708a0beeea12a3620efdfb6c31975471916b463d9f48aade356bfe1cf"}, + {file = "granian-2.3.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:15f915f9d8b64721cbeacb2081101c5bcf33b57b4fe121e5517e0d0317f09335"}, + {file = "granian-2.3.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e371c43136f16083594ee414fbb95fb437abd835d40c594e5c14a755945ce273"}, + {file = "granian-2.3.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9e95d31f48132b1ed267203291bfd523106838508d49fedafdc9ba303c80e775"}, + {file = "granian-2.3.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9f869cc23b96d634d9f2ca1880fcffa4634fbedfb7ab9b587e41568e40edcb88"}, + {file = "granian-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:47d44e427d995e22d65ba3674ca9255233e912de7ae384762c0d0d3f5d634ba1"}, + {file = "granian-2.3.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64ca0cceafc0640c09a4cc066da6ef3af324a1e7cfd7d9c7f3b4fc3b3cc97d54"}, + {file = "granian-2.3.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a83b08a3b26079ef03bddf4ecd63a2199cd4dc94097a7faf0bf99526d9766e8c"}, + {file = "granian-2.3.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f0396686f44f04d874c849aafc91e559d3d22db2812d68ca619536211f3c2ef2"}, + {file = "granian-2.3.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:6d2032a331e833d3291597dcd7e926c4563b6fab110877d84aeadeed70ee0d23"}, + {file = "granian-2.3.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:89c44de0fbe705191107c8a87f9fb66af5be8125a82b6736af72ef81e2f991d6"}, + {file = "granian-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ec793853e6dec77429ca551ce3028768225f93b569a9cda54bec9929797afb4a"}, + {file = "granian-2.3.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:60080d8c9726f558b91fb8b391badf55390500c1c76b2f85cc7862183360ee0c"}, + {file = "granian-2.3.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d96c8269e733a4176eb39fdc576c361657683d5a1a9b2d9298403b475bb9527c"}, + {file = "granian-2.3.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78ce93e65f20b4bc8d8d53fb9e13b8e12309f75bf2d9695b641b504f137ec4a1"}, + {file = "granian-2.3.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5e04e6d5c91146c9345ed627badb53334c0abd774c653b00569576984ce2"}, + {file = "granian-2.3.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eebef031998b6fbdb7cc81a7b7b1c3166e9f26b067ba315d87b0a5ec21a78d3c"}, + {file = "granian-2.3.4-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:dc1494ecec5e592771c8b3858fb74803d2fc68bc3228280607ea64c4b715b50f"}, + {file = "granian-2.3.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:6e4b88fc6a9cae9813776d1d7b7d78a4959b33ef38443cf24acd97b6addf99db"}, + {file = "granian-2.3.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:76cd8c85aba6d5c3ac0ac834dbb8044de2e734a6b6a7912d3e97d8deabd51744"}, + {file = "granian-2.3.4.tar.gz", hash = "sha256:ed04cb7f9befb1bcb8f97b7fee6d19eb4375346eeddd43c77193c35e9cf66b9a"}, +] + +[package.dependencies] +click = ">=8.0.0" + +[package.extras] +all = ["granian[pname,reload]"] +pname = ["setproctitle (>=1.3.3,<1.4.0)"] +reload = ["watchfiles (>=1.0,<2.0)"] +rloop = ["rloop (>=0.1,<1.0) ; sys_platform != \"win32\""] +uvloop = ["uvloop (>=0.18.0) ; platform_python_implementation == \"CPython\" and sys_platform != \"win32\""] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "h2" +version = "4.2.0" +description = "Pure-Python HTTP/2 protocol implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, + {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, +] + +[package.dependencies] +hpack = ">=4.1,<5" +hyperframe = ">=6.1,<7" + +[[package]] +name = "hpack" +version = "4.1.0" +description = "Pure-Python HPACK header encoding" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, + {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, +] + +[[package]] +name = "hypercorn" +version = "0.17.3" +description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "hypercorn-0.17.3-py3-none-any.whl", hash = "sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547"}, + {file = "hypercorn-0.17.3.tar.gz", hash = "sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165"}, +] + +[package.dependencies] +h11 = "*" +h2 = ">=3.1.0" +priority = "*" +wsproto = ">=0.14.0" + +[package.extras] +docs = ["pydata_sphinx_theme", "sphinxcontrib_mermaid"] +h3 = ["aioquic (>=0.9.0,<1.0)"] +trio = ["trio (>=0.22.0)"] +uvloop = ["uvloop (>=0.18) ; platform_system != \"Windows\""] + +[[package]] +name = "hyperframe" +version = "6.1.0" +description = "Pure-Python HTTP/2 framing" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, + {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, +] + +[[package]] +name = "invidious-exporter" +version = "0.1.0" +description = "" +optional = false +python-versions = ">=3.13" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +asyncpg = ">=0.30.0,<0.31.0" + +[package.source] +type = "git" +url = "https://git.m724.eu/id.420129/invidious-exporter-python.git" +reference = "282d94bdd4882710c14142bdf667abb6cd6f6fb4" +resolved_reference = "282d94bdd4882710c14142bdf667abb6cd6f6fb4" + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "priority" +version = "2.0.0" +description = "A pure-Python implementation of the HTTP/2 priority tree" +optional = false +python-versions = ">=3.6.1" +groups = ["main"] +files = [ + {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, + {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "quart" +version = "0.20.0" +description = "A Python ASGI web framework with the same API as Flask" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "quart-0.20.0-py3-none-any.whl", hash = "sha256:003c08f551746710acb757de49d9b768986fd431517d0eb127380b656b98b8f1"}, + {file = "quart-0.20.0.tar.gz", hash = "sha256:08793c206ff832483586f5ae47018c7e40bdd75d886fee3fabbdaa70c2cf505d"}, +] + +[package.dependencies] +aiofiles = "*" +blinker = ">=1.6" +click = ">=8.0" +flask = ">=3.0" +hypercorn = ">=0.11.2" +itsdangerous = "*" +jinja2 = "*" +markupsafe = "*" +werkzeug = ">=3.0" + +[package.extras] +dotenv = ["python-dotenv"] + +[[package]] +name = "quart-bcrypt" +version = "0.0.9" +description = "Quart-Bcrypt is a Quart extension that provides bcrypt hashing utilities for your application." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "quart_bcrypt-0.0.9-py3-none-any.whl", hash = "sha256:f541c5eba1ca48269f4e2ba5aad8c4d0bcbb833b355164adbbf0c63dedee999b"}, + {file = "quart_bcrypt-0.0.9.tar.gz", hash = "sha256:b869a7298de6a19cc78f183ff760a79ca38067ca97d3fb8cd4760db9d60b78d9"}, +] + +[package.dependencies] +bcrypt = ">=3.2.0" +quart = ">=0.19.0" + +[[package]] +name = "quart-db" +version = "0.9.0" +description = "Quart-DB is a Quart extension that provides managed connection(s) to database(s)." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "quart_db-0.9.0-py3-none-any.whl", hash = "sha256:95e70e77867d47885f54345fecfea85e333605410519b984588b81eb96fd70ec"}, + {file = "quart_db-0.9.0.tar.gz", hash = "sha256:c088b73927d2060d173ede0e9d81115134a3ea80341e6a23e9d0727c167da78a"}, +] + +[package.dependencies] +asyncpg = ">=0.25.0" +buildpg = ">=0.4" +quart = ">=0.16.3" + +[package.extras] +docs = ["pydata_sphinx_theme"] +erdiagram = ["eralchemy"] +psycopg = ["psycopg (>=3.2)"] +sqlite = ["aiosqlite"] + +[[package]] +name = "quart-rate-limiter" +version = "0.12.0" +description = "A Quart extension to provide rate limiting support" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "quart_rate_limiter-0.12.0-py3-none-any.whl", hash = "sha256:f538cac8fab734ec11e817ecb318e448ddb071e41d7254ef6ad288fc883871c4"}, + {file = "quart_rate_limiter-0.12.0.tar.gz", hash = "sha256:951dc3d6636fc0ce51859228d5cdcbd0db7a1287a1efe92480a0e53183a82612"}, +] + +[package.dependencies] +quart = ">=0.19" + +[package.extras] +docs = ["pydata_sphinx_theme"] +redis = ["redis (>=4.4.0)"] +valkey = ["valkey"] + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +groups = ["main"] +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + +[metadata] +lock-version = "2.1" +python-versions = ">=3.13" +content-hash = "cd9427a4782d4b33f06573254304379fd207ecab81f0afc295e6a49a12dbc214" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2ee9d05 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "invidious-export-server" +version = "0.1.0" +description = "" +authors = [ + {name = "Minecon724",email = "minecon724@noreply.git.m724.eu"} +] +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "quart (>=0.20.0,<0.21.0)", + "quart-db (>=0.9.0,<0.10.0)", + "quart-bcrypt (>=0.0.9,<0.0.10)", + "invidious-exporter @ git+https://git.m724.eu/id.420129/invidious-exporter-python.git@282d94bdd4882710c14142bdf667abb6cd6f6fb4", + "quart-rate-limiter (>=0.12.0,<0.13.0)", + "granian (>=2.3.4,<3.0.0)", + "python-dotenv (>=1.1.0,<2.0.0)", +] + +[tool.poetry] +packages = [{include = "invidious_export_server", from = "src"}] + +[tool.poetry.scripts] +start = "invidious_export_server:run" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/invidious_export_server/__init__.py b/src/invidious_export_server/__init__.py new file mode 100644 index 0000000..570b19a --- /dev/null +++ b/src/invidious_export_server/__init__.py @@ -0,0 +1,59 @@ +from quart import Quart, redirect, request, render_template, make_response, url_for +from quart_db import QuartDB +from quart_bcrypt import Bcrypt +from quart_rate_limiter import RateLimiter + +from os import environ +from dotenv import load_dotenv + +from . import blueprints + +app = Quart(__name__) + +load_dotenv() +app.config.from_prefixed_env() +app.config['QUART_DB_DATABASE_URL'] = environ.get("DATABASE_URL") +app.config['ALTERNATIVE_INSTANCE_URL'] = environ.get("ALTERNATIVE_INSTANCE_URL", "https://redirect.invidious.io") + +db = QuartDB(app) +bcrypt = Bcrypt(app) +rate_limiter = RateLimiter(app) + +blueprints.register_all(app) + +@app.context_processor +def add_variables(): + return { + 'alternative_instance_url': app.config['ALTERNATIVE_INSTANCE_URL'], + 'authenticated': request.cookies.get('SID') is not None # rough check + } + +@app.route('/') +async def index(): + return await render_template('index.html') + +@app.route('/') +async def catch_all(path: str): + path = request.full_path + if request.cookies.get('SID') is not None: # whether the user is supposed to be authenticated is a good enough clue + return await render_template('index.html', path=path) + + response = await make_response(redirect("https://redirect.invidious.io" + path)) + response.set_cookie('exit', path) + + return response + +@app.route('/api/') +def catch_all_api(path): + return "This Invidious instance is shutting down. For more information, visit https://id.420129.xyz" + +@app.route('/favicon.ico') +def favicon(): + return redirect(url_for("static", filename="favicon.webp")) + +@app.route('/health') +def health(): + return 'OK' + +def run() -> None: + app.run(debug=True) diff --git a/src/invidious_export_server/authenticator.py b/src/invidious_export_server/authenticator.py new file mode 100644 index 0000000..5763287 --- /dev/null +++ b/src/invidious_export_server/authenticator.py @@ -0,0 +1,47 @@ +from quart import g +from quart_bcrypt import async_check_password_hash + +from os import urandom +from base64 import urlsafe_b64encode +from datetime import datetime, timezone + +async def is_authenticated(session_id: str) -> bool: + return await get_username(session_id) is not None + +async def get_username(session_id: str) -> str | None: + if session_id is None: + return None + + username = await g.connection.fetch_val( + "SELECT email FROM session_ids WHERE id = :id", + {"id": session_id}, + ) + + return username + +async def generate_session_id(username: str, password: str) -> str | None: + if username is None or password is None: + return None + + username = username.lower() + + user_password = await g.connection.fetch_val( + "SELECT password FROM users WHERE email = :email", + {"email": username}, + ) + + if user_password is None: + return None + + if not await async_check_password_hash(user_password, password): + return None + + session_id = urlsafe_b64encode(urandom(32)).decode('utf-8') + + async with g.connection.transaction(): + await g.connection.execute( + "INSERT INTO session_ids (id, email, issued) VALUES (:id, :email, :issued)", + {"id": session_id, "email": username, "issued": datetime.now(timezone.utc)} + ) + + return session_id \ No newline at end of file diff --git a/src/invidious_export_server/blueprints/__init__.py b/src/invidious_export_server/blueprints/__init__.py new file mode 100644 index 0000000..727b56e --- /dev/null +++ b/src/invidious_export_server/blueprints/__init__.py @@ -0,0 +1,11 @@ + +from quart import Quart + +from . import auth, control + + +__all__ = [auth, control] + +def register_all(app: Quart): + app.register_blueprint(auth.blueprint) + app.register_blueprint(control.blueprint) \ No newline at end of file diff --git a/src/invidious_export_server/blueprints/auth/__init__.py b/src/invidious_export_server/blueprints/auth/__init__.py new file mode 100644 index 0000000..cc3c962 --- /dev/null +++ b/src/invidious_export_server/blueprints/auth/__init__.py @@ -0,0 +1,44 @@ +from quart import Blueprint, redirect, url_for, flash, make_response, request, render_template +from quart_rate_limiter import rate_limit + +from datetime import timedelta + +from ... import authenticator + + +blueprint = Blueprint('auth', __name__, + url_prefix='/', + template_folder='templates') + +@blueprint.route('/login', methods=['GET']) +async def login(): + if await authenticator.is_authenticated(request.cookies.get('SID')): + return redirect(url_for('control.data_control')) + + return await render_template('login.html') + +@blueprint.route('/login', methods=['POST']) +@rate_limit(5, timedelta(seconds=5)) +async def login_post(): + form = await request.form + + username = form.get('username') + password = form.get('password') + + session_id = await authenticator.generate_session_id(username, password) + + if session_id is None: + await flash("Invalid credentials") + return redirect(url_for('auth.login')) + + response = await make_response(redirect(url_for('control.data_control'))) + response.set_cookie('SID', session_id) + + return response + +@blueprint.route('/logout') +async def logout(): + response = await make_response(redirect(url_for('index'))) + response.delete_cookie('SID') + + return response \ No newline at end of file diff --git a/src/invidious_export_server/blueprints/auth/templates/login.html b/src/invidious_export_server/blueprints/auth/templates/login.html new file mode 100644 index 0000000..05c337a --- /dev/null +++ b/src/invidious_export_server/blueprints/auth/templates/login.html @@ -0,0 +1,19 @@ +{% extends "layout.html" %} + +{% block content %} + +
+ + + +
+ + + + +
+ + +
+ +{% endblock %} \ No newline at end of file diff --git a/src/invidious_export_server/blueprints/control/__init__.py b/src/invidious_export_server/blueprints/control/__init__.py new file mode 100644 index 0000000..0e48b7d --- /dev/null +++ b/src/invidious_export_server/blueprints/control/__init__.py @@ -0,0 +1,88 @@ +from quart import Blueprint, redirect, url_for, request, render_template, make_response, abort, g +from quart_rate_limiter import rate_limit + +from datetime import timedelta +from invidious_exporter.objects import InvidiousJsonDataExport +from json import loads +from urllib.parse import unquote + +from ... import authenticator +from ...export import export_user + + +blueprint = Blueprint('control', __name__, + url_prefix='/', + template_folder='templates') + + +@blueprint.route('/preferences') +def preferences(): + return redirect(url_for('control.data_control')) + +@blueprint.route('/data_control') +async def data_control(): + username = await authenticator.get_username(request.cookies.get('SID')) + + if not username: + has_prefs = 'PREFS' in request.cookies + return await render_template('data_control_guest.html', username=username, has_prefs=has_prefs) + + return await render_template('data_control.html', username=username) + +@blueprint.route('/subscription_manager') +@rate_limit(1, timedelta(seconds=1)) # because this is quite heavy +async def subscription_manager(): + if request.args.get('format') == 'json': + username = await authenticator.get_username(request.cookies.get('SID')) + + if username is None: + preferences_serialized = request.cookies.get("PREFS") + data_export = export_guest(loads(unquote(preferences_serialized))) + + username = "A guest" + else: + async with g.connection.transaction(): + data_export = await export_user(g.connection, username) + + response = await make_response(str(data_export.to_dict())) + response.headers['Content-Disposition'] = f"attachment; filename={username}'s id.420129.xyz data export.json" + + return response + + abort(400, "Format must be json") + +@blueprint.route('/delete_account') +async def delete_account(): + username = await authenticator.get_username(request.cookies.get('SID')) + if username is None: + abort(401) + + # invidious doesn't have on delete cascade. You could make migrations, but believe me, you don't want to. + + async with g.connection.transaction(): + await g.connection.execute(""" + DELETE FROM playlist_videos + WHERE index IN ( + SELECT UNNEST(index) + FROM playlists + WHERE author = :author + ) + """, {"author": username}) + + await g.connection.execute("DELETE FROM playlists WHERE author = :username", {"username": username}) + await g.connection.execute("DELETE FROM session_ids WHERE email = :username", {"username": username}) + await g.connection.execute("DELETE FROM users WHERE email = :username", {"username": username}) + + response = await make_response(redirect(request.cookies.get('exit', '/'))) + response.delete_cookie('SID') + + return response + + +def export_guest(preferences: dict) -> InvidiousJsonDataExport: + return InvidiousJsonDataExport( + subscriptions=[], + watch_history=[], + playlists=[], + preferences=preferences + ) \ No newline at end of file diff --git a/src/invidious_export_server/blueprints/control/templates/data_control.html b/src/invidious_export_server/blueprints/control/templates/data_control.html new file mode 100644 index 0000000..704bb8c --- /dev/null +++ b/src/invidious_export_server/blueprints/control/templates/data_control.html @@ -0,0 +1,35 @@ +{% extends "layout.html" %} + +{% block content %} + +

There's not much left to manage.

+ +

Here's what you should do:

+ +
    +
  1. +

    + Download your data off id.420129.xyz. +
    + Click here to export your id.420129.xyz account data. +

    +
  2. +
  3. +

    + Import your data onto another Invidious instance. +
    + Click here to read how. +

    +
  4. +
  5. +

    + Finally, delete your account. +
    + Click here to delete your id.420129.xyz account. +

    +
  6. +
+ +

Thank you for having an account with us.

+ +{% endblock %} \ No newline at end of file diff --git a/src/invidious_export_server/blueprints/control/templates/data_control_guest.html b/src/invidious_export_server/blueprints/control/templates/data_control_guest.html new file mode 100644 index 0000000..eb41081 --- /dev/null +++ b/src/invidious_export_server/blueprints/control/templates/data_control_guest.html @@ -0,0 +1,32 @@ +{% extends "layout.html" %} + +{% block content %} + +

You're not logged in. If you have an account with id.420129.xyz, click here to log in. + + {% if has_prefs %} +

However, you can still export your saved preferences for use with another Invidious instance.

+ +

Here's what you should do:

+ +
    +
  1. +

    + Download your preferences off id.420129.xyz. +
    + Click here to export your id.420129.xyz account preferences. +

    +
  2. +
  3. +

    + Import your data onto another Invidious instance. +
    + Click here to read how. +

    +
  4. +
+ {% else %} +

Right now, you don't have anything to export.

+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/src/invidious_export_server/export.py b/src/invidious_export_server/export.py new file mode 100644 index 0000000..ec507af --- /dev/null +++ b/src/invidious_export_server/export.py @@ -0,0 +1,40 @@ +from json import loads +from quart_db import Connection + +from invidious_exporter.database import create_json_data_export, create_playlists +from invidious_exporter.objects import Playlist, InvidiousJsonDataExport + +# This is the exact same except with the .fetches changed + +async def get_playlists(connection: Connection, user_email: str) -> list[Playlist]: + sql = """ + SELECT + p.title, + p.description, + p.privacy, + ARRAY_AGG(pv.id) FILTER (WHERE pv.id IS NOT NULL) AS video_ids + FROM + playlists AS p + LEFT JOIN + playlist_videos AS pv ON pv.index = ANY(p.index) + WHERE + p.author = :author + GROUP BY + p.index, p.title, p.description, p.privacy; + """ + + rows = await connection.fetch_all( + sql, + {"author": user_email} + ) + + return create_playlists(rows) + +async def export_user(connection: Connection, user_email: str) -> InvidiousJsonDataExport: + data = await connection.fetch_one( + "SELECT subscriptions, watched, preferences FROM users WHERE email = :email", + {"email": user_email} + ) + playlists = await get_playlists(connection, user_email) + + return create_json_data_export(data, playlists) \ No newline at end of file diff --git a/src/invidious_export_server/static/favicon.webp b/src/invidious_export_server/static/favicon.webp new file mode 100644 index 0000000000000000000000000000000000000000..e6d19f10c53dc199c2a30bf488b33c4a99bfd42f GIT binary patch literal 808 zcmV+@1K0dgNk&E>0{{S5MM6+kP&il$0000G0000l001ul06|PpNR$Bp00EF-+qUUS zC-}B)+qUhpZ5utXZQHhO+qUE67GLh$Er^H-Af7Ti-^hf7NMCz70ZQFVMz1_^=lPpA z&+ndEJ+{n4Dvo8rwCf5cSb1w!i8Viqn(bd1$M3rvt@uddSN&j|e{M*Ta4_%jgo*f~ zzok)5Qx!~9)m%3t+*j+F$cD{6f?U=zz^xtvtmYUH*izg7NCvAJq;{+lz)KSz0+PygFl*n*Q&&+yJn_jfA=`)rqBF>q)}x z$NdmTG0mBbHS2g^jK@7Jh-1{^ue`pmjF%8sv(ojSd3@PYWkKA{v&LQeF6_tENx7CJ zs>C^W(4s@vA3l5b=*E#HL-JiE1Sqxk2nq`e^0bi>PgYPkAZ7so0PqL^odGH^05AYP zJ5c6AF5(3a-x@9 zQUz^Sb-WM2hjbyLWPkSX0092EE9dXTv)*-lsdzRo`Bxi-M3sa@@w4KY;6d9j7ljoM zV?c?HEXCOTh@k+1GV7>iv%f3eJJVt{i8G>0&d%Ee)lUn)2jAZPF8w=vG%N5mt>Jo$ zi$8m=|JxZ-$z!1|5`~W^3bd739)V*oj1BbFNByNERdbE<(<1x5Z6{M`Xn_{)$@ z;|?dfjzH6x@wEJPY;pJ#qfG(!-!$xZQ3Yw7IFy|c1AJwmq6`%R&*`>`Vwa^!qswbr zPb#4-x%r4EKodmlu7YVbO_ mwd<7gg4?h%Ls+bB4rquP63U=lSlrXMk?mN}>W=P=0001|-Gz++ literal 0 HcmV?d00001 diff --git a/src/invidious_export_server/static/style.css b/src/invidious_export_server/static/style.css new file mode 100644 index 0000000..19027f9 --- /dev/null +++ b/src/invidious_export_server/static/style.css @@ -0,0 +1,60 @@ +html, body { + background-color: #232323; + color: #f0f0f0; + + font-family: sans-serif; + width: 100%; + margin: 0; +} + +a { + color: #adadad; + text-decoration: none; + + &:hover { + color: #00b6f0; + } +} + +.header-link { + color: #f0f0f0; + + height: 2.5em; + display: flex; + flex-direction: column; + justify-content: center; +} + +#app { + margin: 0 auto; + + width: calc(100% - 2em); + + @media screen and (min-width: 80em) { + width: calc(83.3333% - 2em); /* copied from invidious */ + } + + header { + margin: 1em 0; + height: 2.5em; + + display: flex; + justify-content: space-between; + + #invidious-link { + font-weight: bold; + } + + #manage-buttons { + display: flex; + + #preferences-link { + margin-right: 1em; + + #cog-icon { + width: 1em; + } + } + } + } +} \ No newline at end of file diff --git a/src/invidious_export_server/templates/index.html b/src/invidious_export_server/templates/index.html new file mode 100644 index 0000000..e995e0f --- /dev/null +++ b/src/invidious_export_server/templates/index.html @@ -0,0 +1,15 @@ +{% extends "layout.html" %} + +{% block content %} + +

id.420129.xyz is no longer available.

+ +

After which you should try to:

+ + + +{% endblock %} \ No newline at end of file diff --git a/src/invidious_export_server/templates/layout.html b/src/invidious_export_server/templates/layout.html new file mode 100644 index 0000000..e92f66c --- /dev/null +++ b/src/invidious_export_server/templates/layout.html @@ -0,0 +1,48 @@ + + + + + + + + + Invidious + + +
+
+ INVIDIOUS + +
+ + + + + {% if authenticated %} + + LOG OUT + {% if username %} +
+ {{ username }} + {% endif %} +
+ {% else %} + LOG IN + {% endif %} +
+
+ + {% with messages = get_flashed_messages() %} + {% if messages %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+ + \ No newline at end of file