diff --git a/.python-version b/.python-version index 24ee5b1..e4fba21 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.13 +3.12 diff --git a/Dockerfile b/Dockerfile index 5510349..66014c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,24 +13,20 @@ WORKDIR /var/app COPY build/requirements.txt ./ -RUN pip install --no-cache-dir \ - --require-hashes \ - --requirement requirements.txt +RUN pip install --no-cache-dir --upgrade \ + --requirement requirements.txt USER 10000:10001 -COPY alembic.ini entrypoint.sh pyproject.toml run ./ -COPY alembic ./alembic +COPY run ./ COPY scripts ./scripts COPY unwind ./unwind -RUN pip install --no-cache-dir --editable . - ENV UNWIND_DATA="/data" VOLUME $UNWIND_DATA ENV UNWIND_PORT=8097 EXPOSE $UNWIND_PORT -ENTRYPOINT ["/var/app/entrypoint.sh"] +ENTRYPOINT ["/var/app/run"] CMD ["server"] diff --git a/alembic.ini b/alembic.ini deleted file mode 100644 index d8a741e..0000000 --- a/alembic.ini +++ /dev/null @@ -1,39 +0,0 @@ -[alembic] -script_location = alembic -file_template = %%(epoch)s-%%(rev)s_%%(slug)s -timezone = UTC - -# Logging configuration -[loggers] -keys = root,sqlalchemy,alembic - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = WARN -handlers = console -qualname = - -[logger_sqlalchemy] -level = WARN -handlers = -qualname = sqlalchemy.engine - -[logger_alembic] -level = INFO -handlers = -qualname = alembic - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py deleted file mode 100644 index b3ea427..0000000 --- a/alembic/env.py +++ /dev/null @@ -1,108 +0,0 @@ -import asyncio -from logging.config import fileConfig - -import sqlalchemy as sa -from sqlalchemy import pool -from sqlalchemy.engine import Connection -from sqlalchemy.ext.asyncio import async_engine_from_config - -from alembic import context -from unwind import db, models - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config - -# Interpret the config file for Python logging. -# This line sets up loggers basically. -if config.config_file_name is not None: - fileConfig(config.config_file_name) - - -def is_different_type( - context, - inspected_column: sa.Column, - metadata_column: sa.Column, - inspected_type: sa.types.TypeEngine, - metadata_type: sa.types.TypeEngine, -) -> bool | None: - # We used "TEXT" in our manual SQL, which in SQLite is the same as VARCHAR, but - # for SQLAlchemy/Alembic looks different. - equiv_types = [(sa.TEXT, sa.String)] - for types in equiv_types: - if isinstance(inspected_type, types) and isinstance(metadata_type, types): - return False - return None # defer to default compare implementation - - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - context.configure( - url=db._connection_uri(), - target_metadata=models.metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - compare_type=is_different_type, - render_as_batch=True, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def do_run_migrations(connection: Connection) -> None: - context.configure( - connection=connection, - target_metadata=models.metadata, - compare_type=is_different_type, - render_as_batch=True, - ) - - with context.begin_transaction(): - context.run_migrations() - - -async def run_async_migrations() -> None: - """In this scenario we need to create an Engine - and associate a connection with the context. - - """ - - connectable = async_engine_from_config( - config.get_section(config.config_ini_section, {}), - prefix="sqlalchemy.", - url=db._connection_uri(), - poolclass=pool.NullPool, - ) - - async with connectable.connect() as connection: - await connection.run_sync(do_run_migrations) - - await connectable.dispose() - - -def run_migrations_online() -> None: - """Run migrations in 'online' mode.""" - # Support having a (sync) connection passed in from another script. - if (conn := config.attributes.get("connection")) and isinstance( - conn, sa.Connection - ): - do_run_migrations(conn) - else: - asyncio.run(run_async_migrations()) - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako deleted file mode 100644 index f31592a..0000000 --- a/alembic/script.py.mako +++ /dev/null @@ -1,26 +0,0 @@ -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from typing import Sequence - -from alembic import op -import sqlalchemy as sa -${imports if imports else ""} - -# revision identifiers, used by Alembic. -revision: str = ${repr(up_revision)} -down_revision: str | None = ${repr(down_revision)} -branch_labels: str | Sequence[str] | None = ${repr(branch_labels)} -depends_on: str | Sequence[str] | None = ${repr(depends_on)} - - -def upgrade() -> None: - ${upgrades if upgrades else "pass"} - - -def downgrade() -> None: - ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/1716049471-c08ae04dc482_fix_data_types.py b/alembic/versions/1716049471-c08ae04dc482_fix_data_types.py deleted file mode 100644 index 5a72f3a..0000000 --- a/alembic/versions/1716049471-c08ae04dc482_fix_data_types.py +++ /dev/null @@ -1,69 +0,0 @@ -"""fix data types - -Revision ID: c08ae04dc482 -Revises: -Create Date: 2024-05-18 16:24:31.152480+00:00 - -""" - -from typing import Sequence - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "c08ae04dc482" -down_revision: str | None = None -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("ratings", schema=None) as batch_op: - batch_op.alter_column( - "score", - existing_type=sa.NUMERIC(), - type_=sa.Integer(), - existing_nullable=False, - ) - batch_op.alter_column( - "favorite", - existing_type=sa.NUMERIC(), - type_=sa.Integer(), - existing_nullable=True, - ) - batch_op.alter_column( - "finished", - existing_type=sa.NUMERIC(), - type_=sa.Integer(), - existing_nullable=True, - ) - - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("ratings", schema=None) as batch_op: - batch_op.alter_column( - "finished", - existing_type=sa.Integer(), - type_=sa.NUMERIC(), - existing_nullable=True, - ) - batch_op.alter_column( - "favorite", - existing_type=sa.Integer(), - type_=sa.NUMERIC(), - existing_nullable=True, - ) - batch_op.alter_column( - "score", - existing_type=sa.Integer(), - type_=sa.NUMERIC(), - existing_nullable=False, - ) - - # ### end Alembic commands ### diff --git a/alembic/versions/1716050110-62882ef5e3ff_add_awards_table.py b/alembic/versions/1716050110-62882ef5e3ff_add_awards_table.py deleted file mode 100644 index b66cee0..0000000 --- a/alembic/versions/1716050110-62882ef5e3ff_add_awards_table.py +++ /dev/null @@ -1,44 +0,0 @@ -"""add awards table - -Revision ID: 62882ef5e3ff -Revises: c08ae04dc482 -Create Date: 2024-05-18 16:35:10.145964+00:00 - -""" - -from typing import Sequence - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "62882ef5e3ff" -down_revision: str | None = "c08ae04dc482" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "awards", - sa.Column("id", sa.String(), nullable=False), - sa.Column("movie_id", sa.String(), nullable=False), - sa.Column("category", sa.String(), nullable=False), - sa.Column("details", sa.String(), nullable=False), - sa.Column("created", sa.String(), nullable=False), - sa.Column("updated", sa.String(), nullable=False), - sa.ForeignKeyConstraint( - ["movie_id"], - ["movies.id"], - ), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("awards") - # ### end Alembic commands ### diff --git a/alembic/versions/1716051987-f17c7ca9afa4_use_named_constraints.py b/alembic/versions/1716051987-f17c7ca9afa4_use_named_constraints.py deleted file mode 100644 index be21664..0000000 --- a/alembic/versions/1716051987-f17c7ca9afa4_use_named_constraints.py +++ /dev/null @@ -1,41 +0,0 @@ -"""use named constraints - -See https://alembic.sqlalchemy.org/en/latest/naming.html - -Revision ID: f17c7ca9afa4 -Revises: 62882ef5e3ff -Create Date: 2024-05-18 17:06:27.696713+00:00 - -""" - -from typing import Sequence - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "f17c7ca9afa4" -down_revision: str | None = "62882ef5e3ff" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("movies") as batch_op: - batch_op.create_unique_constraint(batch_op.f("uq_movies_imdb_id"), ["imdb_id"]) - - with op.batch_alter_table("users", schema=None) as batch_op: - batch_op.create_unique_constraint(batch_op.f("uq_users_imdb_id"), ["imdb_id"]) - - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("users", schema=None) as batch_op: - batch_op.drop_constraint(batch_op.f("uq_users_imdb_id"), type_="unique") - - with op.batch_alter_table("movies", schema=None) as batch_op: - batch_op.drop_constraint(batch_op.f("uq_movies_imdb_id"), type_="unique") - - # ### end Alembic commands ### diff --git a/alembic/versions/1716077466-8b06e4916840_remove_db_patches_table.py b/alembic/versions/1716077466-8b06e4916840_remove_db_patches_table.py deleted file mode 100644 index 840cb33..0000000 --- a/alembic/versions/1716077466-8b06e4916840_remove_db_patches_table.py +++ /dev/null @@ -1,38 +0,0 @@ -"""remove db_patches table - -We replace our old patch process with Alembic's. - -Revision ID: 8b06e4916840 -Revises: f17c7ca9afa4 -Create Date: 2024-05-19 00:11:06.730421+00:00 - -""" - -from typing import Sequence - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision: str = "8b06e4916840" -down_revision: str | None = "f17c7ca9afa4" -branch_labels: str | Sequence[str] | None = None -depends_on: str | Sequence[str] | None = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("db_patches") - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "db_patches", - sa.Column("id", sa.INTEGER(), nullable=False), - sa.Column("current", sa.VARCHAR(), nullable=True), - sa.PrimaryKeyConstraint("id"), - ) - # ### end Alembic commands ### diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100755 index 7df7daa..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -eu - -alembic upgrade head -exec ./run "$@" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..dc71990 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,697 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "aiosqlite" +version = "0.20.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, + {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, +] + +[package.dependencies] +typing_extensions = ">=4.0" + +[package.extras] +dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "beautifulsoup4" +version = "4.12.3" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, +] + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[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" +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 = "coverage" +version = "7.5.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, + {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, + {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, + {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, + {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, + {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, + {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, + {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, + {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, + {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, + {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, + {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, + {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "greenlet" +version = "3.0.3" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "honcho" +version = "1.1.0" +description = "Honcho: a Python clone of Foreman. For managing Procfile-based applications." +optional = false +python-versions = "*" +files = [ + {file = "honcho-1.1.0-py2.py3-none-any.whl", hash = "sha256:a4d6e3a88a7b51b66351ecfc6e9d79d8f4b87351db9ad7e923f5632cc498122f"}, + {file = "honcho-1.1.0.tar.gz", hash = "sha256:c5eca0bded4bef6697a23aec0422fd4f6508ea3581979a3485fc4b89357eb2a9"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +export = ["jinja2 (>=2.7,<3)"] + +[[package]] +name = "html5lib" +version = "1.1" +description = "HTML parser based on the WHATWG HTML specification" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d"}, + {file = "html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f"}, +] + +[package.dependencies] +six = ">=1.9" +webencodings = "*" + +[package.extras] +all = ["chardet (>=2.2)", "genshi", "lxml"] +chardet = ["chardet (>=2.2)"] +genshi = ["genshi"] +lxml = ["lxml"] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pyright" +version = "1.1.362" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.362-py3-none-any.whl", hash = "sha256:969957cff45154d8a45a4ab1dae5bdc8223d8bd3c64654fa608ab3194dfff319"}, + {file = "pyright-1.1.362.tar.gz", hash = "sha256:6a477e448d4a07a6a0eab58b2a15a1bbed031eb3169fa809edee79cca168d83a"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "pytest" +version = "8.2.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, + {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.6" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, + {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "ruff" +version = "0.4.3" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"}, + {file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"}, + {file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"}, + {file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"}, + {file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"}, + {file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"}, + {file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"}, + {file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"}, +] + +[[package]] +name = "setuptools" +version = "69.5.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.30" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b48154678e76445c7ded1896715ce05319f74b1e73cf82d4f8b59b46e9c0ddc"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2753743c2afd061bb95a61a51bbb6a1a11ac1c44292fad898f10c9839a7f75b2"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7bfc726d167f425d4c16269a9a10fe8630ff6d14b683d588044dcef2d0f6be7"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f61ada6979223013d9ab83a3ed003ded6959eae37d0d685db2c147e9143797"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a365eda439b7a00732638f11072907c1bc8e351c7665e7e5da91b169af794af"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bba002a9447b291548e8d66fd8c96a6a7ed4f2def0bb155f4f0a1309fd2735d5"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-win32.whl", hash = "sha256:0138c5c16be3600923fa2169532205d18891b28afa817cb49b50e08f62198bb8"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-win_amd64.whl", hash = "sha256:99650e9f4cf3ad0d409fed3eec4f071fadd032e9a5edc7270cd646a26446feeb"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:955991a09f0992c68a499791a753523f50f71a6885531568404fa0f231832aa0"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f69e4c756ee2686767eb80f94c0125c8b0a0b87ede03eacc5c8ae3b54b99dc46"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c9db1ce00e59e8dd09d7bae852a9add716efdc070a3e2068377e6ff0d6fdaa"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1429a4b0f709f19ff3b0cf13675b2b9bfa8a7e79990003207a011c0db880a13"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:efedba7e13aa9a6c8407c48facfdfa108a5a4128e35f4c68f20c3407e4376aa9"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16863e2b132b761891d6c49f0a0f70030e0bcac4fd208117f6b7e053e68668d0"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-win32.whl", hash = "sha256:2ecabd9ccaa6e914e3dbb2aa46b76dede7eadc8cbf1b8083c94d936bcd5ffb49"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-win_amd64.whl", hash = "sha256:0b3f4c438e37d22b83e640f825ef0f37b95db9aa2d68203f2c9549375d0b2260"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5a79d65395ac5e6b0c2890935bad892eabb911c4aa8e8015067ddb37eea3d56c"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a5baf9267b752390252889f0c802ea13b52dfee5e369527da229189b8bd592e"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cb5a646930c5123f8461f6468901573f334c2c63c795b9af350063a736d0134"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:296230899df0b77dec4eb799bcea6fbe39a43707ce7bb166519c97b583cfcab3"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c62d401223f468eb4da32627bffc0c78ed516b03bb8a34a58be54d618b74d472"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3b69e934f0f2b677ec111b4d83f92dc1a3210a779f69bf905273192cf4ed433e"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-win32.whl", hash = "sha256:77d2edb1f54aff37e3318f611637171e8ec71472f1fdc7348b41dcb226f93d90"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-win_amd64.whl", hash = "sha256:b6c7ec2b1f4969fc19b65b7059ed00497e25f54069407a8701091beb69e591a5"}, + {file = "SQLAlchemy-2.0.30-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a8e3b0a7e09e94be7510d1661339d6b52daf202ed2f5b1f9f48ea34ee6f2d57"}, + {file = "SQLAlchemy-2.0.30-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b60203c63e8f984df92035610c5fb76d941254cf5d19751faab7d33b21e5ddc0"}, + {file = "SQLAlchemy-2.0.30-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1dc3eabd8c0232ee8387fbe03e0a62220a6f089e278b1f0aaf5e2d6210741ad"}, + {file = "SQLAlchemy-2.0.30-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:40ad017c672c00b9b663fcfcd5f0864a0a97828e2ee7ab0c140dc84058d194cf"}, + {file = "SQLAlchemy-2.0.30-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e42203d8d20dc704604862977b1470a122e4892791fe3ed165f041e4bf447a1b"}, + {file = "SQLAlchemy-2.0.30-cp37-cp37m-win32.whl", hash = "sha256:2a4f4da89c74435f2bc61878cd08f3646b699e7d2eba97144030d1be44e27584"}, + {file = "SQLAlchemy-2.0.30-cp37-cp37m-win_amd64.whl", hash = "sha256:b6bf767d14b77f6a18b6982cbbf29d71bede087edae495d11ab358280f304d8e"}, + {file = "SQLAlchemy-2.0.30-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc0c53579650a891f9b83fa3cecd4e00218e071d0ba00c4890f5be0c34887ed3"}, + {file = "SQLAlchemy-2.0.30-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:311710f9a2ee235f1403537b10c7687214bb1f2b9ebb52702c5aa4a77f0b3af7"}, + {file = "SQLAlchemy-2.0.30-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:408f8b0e2c04677e9c93f40eef3ab22f550fecb3011b187f66a096395ff3d9fd"}, + {file = "SQLAlchemy-2.0.30-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a4b4fb0dd4d2669070fb05b8b8824afd0af57587393015baee1cf9890242d9"}, + {file = "SQLAlchemy-2.0.30-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a943d297126c9230719c27fcbbeab57ecd5d15b0bd6bfd26e91bfcfe64220621"}, + {file = "SQLAlchemy-2.0.30-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a089e218654e740a41388893e090d2e2c22c29028c9d1353feb38638820bbeb"}, + {file = "SQLAlchemy-2.0.30-cp38-cp38-win32.whl", hash = "sha256:fa561138a64f949f3e889eb9ab8c58e1504ab351d6cf55259dc4c248eaa19da6"}, + {file = "SQLAlchemy-2.0.30-cp38-cp38-win_amd64.whl", hash = "sha256:7d74336c65705b986d12a7e337ba27ab2b9d819993851b140efdf029248e818e"}, + {file = "SQLAlchemy-2.0.30-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae8c62fe2480dd61c532ccafdbce9b29dacc126fe8be0d9a927ca3e699b9491a"}, + {file = "SQLAlchemy-2.0.30-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2383146973a15435e4717f94c7509982770e3e54974c71f76500a0136f22810b"}, + {file = "SQLAlchemy-2.0.30-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8409de825f2c3b62ab15788635ccaec0c881c3f12a8af2b12ae4910a0a9aeef6"}, + {file = "SQLAlchemy-2.0.30-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0094c5dc698a5f78d3d1539853e8ecec02516b62b8223c970c86d44e7a80f6c7"}, + {file = "SQLAlchemy-2.0.30-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:edc16a50f5e1b7a06a2dcc1f2205b0b961074c123ed17ebda726f376a5ab0953"}, + {file = "SQLAlchemy-2.0.30-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f7703c2010355dd28f53deb644a05fc30f796bd8598b43f0ba678878780b6e4c"}, + {file = "SQLAlchemy-2.0.30-cp39-cp39-win32.whl", hash = "sha256:1f9a727312ff6ad5248a4367358e2cf7e625e98b1028b1d7ab7b806b7d757513"}, + {file = "SQLAlchemy-2.0.30-cp39-cp39-win_amd64.whl", hash = "sha256:a0ef36b28534f2a5771191be6edb44cc2673c7b2edf6deac6562400288664221"}, + {file = "SQLAlchemy-2.0.30-py3-none-any.whl", hash = "sha256:7108d569d3990c71e26a42f60474b4c02c8586c4681af5fd67e51a044fdea86a"}, + {file = "SQLAlchemy-2.0.30.tar.gz", hash = "sha256:2b1708916730f4830bc69d6f49d37f7698b5bd7530aca7f04f785f8849e95255"}, +] + +[package.dependencies] +aiosqlite = {version = "*", optional = true, markers = "extra == \"aiosqlite\""} +greenlet = {version = "!=0.4.17", optional = true, markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\" or extra == \"aiosqlite\""} +typing-extensions = {version = ">=4.6.0", optional = true, markers = "extra == \"aiosqlite\""} + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + +[[package]] +name = "starlette" +version = "0.37.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "ulid-py" +version = "1.1.0" +description = "Universally Unique Lexicographically Sortable Identifier" +optional = false +python-versions = "*" +files = [ + {file = "ulid-py-1.1.0.tar.gz", hash = "sha256:dc6884be91558df077c3011b9fb0c87d1097cb8fc6534b11f310161afd5738f0"}, + {file = "ulid_py-1.1.0-py2.py3-none-any.whl", hash = "sha256:b56a0f809ef90d6020b21b89a87a48edc7c03aea80e5ed5174172e82d76e3987"}, +] + +[[package]] +name = "uvicorn" +version = "0.29.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, + {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +optional = false +python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "038fed338d6b75c17eb8eb88d36c2411ff936dab23887b70594e5ba1da518451" diff --git a/pyproject.toml b/pyproject.toml index f869584..b230473 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,37 +1,48 @@ [project] name = "unwind" +requires-python = ">=3.12" + +[tool.poetry] +name = "unwind" version = "0" description = "" -authors = [{ name = "ducklet", email = "ducklet@noreply.code.dumpr.org" }] -requires-python = ">=3.13" -license = "blessing" -dependencies = [ - "beautifulsoup4>=4.9.3,<5", - "html5lib~=1.1", - "starlette>=0.46.2", - "ulid-py>=1.1.0,<2", - "uvicorn>=0.34.2", - "httpx>=0.28.1", - "sqlalchemy[aiosqlite]~=2.0", - "alembic>=1.13.1,<2", -] +authors = ["ducklet "] +license = "LOL" -[dependency-groups] -dev = [ - "pytest", - "pyright", - "pytest-asyncio", - "pytest-cov", - "ruff", - "honcho", -] +[tool.poetry.dependencies] +python = "^3.12" +beautifulsoup4 = "^4.9.3" +html5lib = "^1.1" +starlette = "^0.37.2" +ulid-py = "^1.1.0" +uvicorn = "^0.29.0" +httpx = "^0.27.0" +sqlalchemy = {version = "^2.0", extras = ["aiosqlite"]} + +[tool.poetry.group.build.dependencies] +# When we run poetry export, typing-extensions is a transient dependency via +# sqlalchemy, but the hash won't be included in the requirements.txt. +# By making it a direct dependency we can fix this issue, otherwise this could +# be removed. +typing-extensions = "*" + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] +pytest = "*" +pyright = "*" +pytest-asyncio = "*" +pytest-cov = "*" +ruff = "*" +honcho = "*" [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" [tool.pyright] -pythonVersion = "3.13" +pythonVersion = "3.12" [tool.ruff.lint] select = [ diff --git a/scripts/app b/scripts/app index abc0213..f3bc18f 100755 --- a/scripts/app +++ b/scripts/app @@ -4,6 +4,4 @@ cd "$RUN_DIR" [ -z "${DEBUG:-}" ] || set -x -python_bin=$(uv run command -v python) - -exec "$python_bin" -m unwind "$@" +exec poetry run python -m unwind "$@" diff --git a/scripts/build-docker b/scripts/build-docker index f229009..b8a6861 100755 --- a/scripts/build-docker +++ b/scripts/build-docker @@ -33,12 +33,9 @@ githash_long=$(git rev-parse HEAD) version="$githash_short" echo "$version" >"$builddir"/version -uv export \ - --frozen \ - --format=requirements.txt \ - --no-dev \ - --no-emit-project \ - >"$builddir"/requirements.txt +poetry export \ + --with=build \ + --output="$builddir"/requirements.txt $DOCKER_BIN build \ --pull \ diff --git a/scripts/install b/scripts/install index e4bbf24..25e424b 100755 --- a/scripts/install +++ b/scripts/install @@ -4,7 +4,7 @@ cd "$RUN_DIR" [ -z "${DEBUG:-}" ] || set -x -uv sync --frozen +poetry install --with=dev --sync cd unwind-ui npm ci diff --git a/scripts/lint-py b/scripts/lint-py index 101779d..ae69e6f 100755 --- a/scripts/lint-py +++ b/scripts/lint-py @@ -4,7 +4,7 @@ cd "$RUN_DIR" [ -z "${DEBUG:-}" ] || set -x -uv run ruff check --fix . ||: -uv run ruff format . +poetry run ruff check --fix . ||: +poetry run ruff format . -uv run pyright +poetry run pyright diff --git a/scripts/outdated b/scripts/outdated index 0b1ee72..a02b59f 100755 --- a/scripts/outdated +++ b/scripts/outdated @@ -4,10 +4,8 @@ cd "$RUN_DIR" [ -z "${DEBUG:-}" ] || set -x -echo '# Uv:' -uv tree --outdated \ -| grep --color=never ' (latest: ' \ -| sed -E 's/^[│├└─ ]*//' +echo '# Poetry:' +poetry show --outdated --top-level --with=build,dev echo ' # Npm:' diff --git a/scripts/tests b/scripts/tests index 932fffc..95b7eca 100755 --- a/scripts/tests +++ b/scripts/tests @@ -11,4 +11,4 @@ trap 'rm "$dbfile" "${dbfile}-shm" "${dbfile}-wal"' EXIT TERM INT QUIT [ -z "${DEBUG:-}" ] || set -x UNWIND_STORAGE="$dbfile" \ - exec uv run pytest --cov "$@" + exec poetry run pytest --cov "$@" diff --git a/scripts/tests-cov-report b/scripts/tests-cov-report new file mode 100755 index 0000000..9bed8ab --- /dev/null +++ b/scripts/tests-cov-report @@ -0,0 +1,4 @@ +#!/bin/sh -eu + +cd "$RUN_DIR" +exec "$RUN_BIN" tests -x --cov-report html:cov_html diff --git a/scripts/update b/scripts/update index 9b0d485..913d3eb 100755 --- a/scripts/update +++ b/scripts/update @@ -13,23 +13,34 @@ cd "$RUN_DIR" [ -z "${DEBUG:-}" ] || set -x -# Uv +# Poetry -uv sync --upgrade +poetry update --with=build,dev -uv tree --outdated --no-dev \ -| sed -nE 's/.*─ ([^ ]+) (v[^ ]+) \(latest: v([^)]+)\)/\1>=\3/p' \ -| xargs uv add +poetry show --outdated --top-level \ +| cut -d ' ' -f 1 \ +| while read -r pkg; do + poetry add "$pkg@latest" +done -uv tree --outdated --only-dev \ -| sed -nE 's/.*─ ([^ ]+) (v[^ ]+) \(latest: v([^)]+)\)/\1>=\3/p' \ -| xargs uv add --dev +poetry show --outdated --top-level --only=build \ +| cut -d ' ' -f 1 \ +| while read -r pkg; do + poetry add --group=build "$pkg@latest" +done + +poetry show --outdated --top-level --only=dev \ +| cut -d ' ' -f 1 \ +| while read -r pkg; do + poetry add --group=dev "$pkg@latest" +done # Npm cd unwind-ui npm update +npm install $(npm outdated --json --silent | jq -r 'keys|map("\(.)@latest")|@sh') npm outdated --json --silent \ | jq -r 'keys|map(@sh"\(.)@latest")|join("\n")' \ diff --git a/tests/fixtures/bottom_100-20240714.html.bz2 b/tests/fixtures/bottom_100-20240714.html.bz2 deleted file mode 100644 index 37ae7f5..0000000 Binary files a/tests/fixtures/bottom_100-20240714.html.bz2 and /dev/null differ diff --git a/tests/fixtures/most_popular_100-20240714.html.bz2 b/tests/fixtures/most_popular_100-20240714.html.bz2 deleted file mode 100644 index 163c644..0000000 Binary files a/tests/fixtures/most_popular_100-20240714.html.bz2 and /dev/null differ diff --git a/tests/fixtures/ratings-ur655321-20240720.gql.json.bz2 b/tests/fixtures/ratings-ur655321-20240720.gql.json.bz2 deleted file mode 100644 index b56b820..0000000 Binary files a/tests/fixtures/ratings-ur655321-20240720.gql.json.bz2 and /dev/null differ diff --git a/tests/fixtures/ratings-ur655321-20240720.html.bz2 b/tests/fixtures/ratings-ur655321-20240720.html.bz2 deleted file mode 100644 index 01a633a..0000000 Binary files a/tests/fixtures/ratings-ur655321-20240720.html.bz2 and /dev/null differ diff --git a/tests/test_db.py b/tests/test_db.py index c22359d..981e65b 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -20,6 +20,14 @@ def a_movie(**kwds) -> models.Movie: return models.Movie(**args) +@pytest.mark.asyncio +async def test_current_patch_level(conn: db.Connection): + patch_level = "some-patch-level" + assert patch_level != await db.current_patch_level(conn) + await db.set_current_patch_level(conn, patch_level) + assert patch_level == await db.current_patch_level(conn) + + @pytest.mark.asyncio async def test_get(conn: db.Connection): m1 = a_movie() diff --git a/tests/test_imdb.py b/tests/test_imdb.py index d8efa61..7f4efcf 100644 --- a/tests/test_imdb.py +++ b/tests/test_imdb.py @@ -30,43 +30,29 @@ def test_score_conversion(score: int): assert score == score_from_imdb_rating(imdb_rating_from_score(score)) -@pytest.mark.parametrize( - "fixture", - ( - ("most_popular_100.html.bz2"), - ("most_popular_100-20240714.html.bz2"), - ), -) @pytest.mark.asyncio -async def test_load_most_popular_100(monkeypatch, fixture: str): - with bz2.open(fixturesdir / fixture, "rb") as f: +async def test_load_most_popular_100(monkeypatch): + with bz2.open(fixturesdir / "most_popular_100.html.bz2", "rb") as f: html = f.read() soup = bs4.BeautifulSoup(html, "html5lib") monkeypatch.setattr(imdb, "asoup_from_url", AsyncMock(return_value=soup)) movie_ids = await imdb.load_most_popular_100() - assert len(set(movie_ids)) == 100 + assert len(movie_ids) == 100 assert all(id_.startswith("tt") for id_ in movie_ids) -@pytest.mark.parametrize( - "fixture", - ( - ("bottom_100.html.bz2"), - ("bottom_100-20240714.html.bz2"), - ), -) @pytest.mark.asyncio -async def test_load_bottom_100(monkeypatch, fixture: str): - with bz2.open(fixturesdir / fixture, "rb") as f: +async def test_load_bottom_100(monkeypatch): + with bz2.open(fixturesdir / "bottom_100.html.bz2", "rb") as f: html = f.read() soup = bs4.BeautifulSoup(html, "html5lib") monkeypatch.setattr(imdb, "asoup_from_url", AsyncMock(return_value=soup)) movie_ids = await imdb.load_bottom_100() - assert len(set(movie_ids)) == 100 + assert len(movie_ids) == 100 assert all(id_.startswith("tt") for id_ in movie_ids) @@ -134,7 +120,7 @@ async def test_load_ratings_page_20240510(monkeypatch): if rating.movie.imdb_id == item["imdb_id"]: rating_dict = {key: getattr(rating.movie, key) for key in item.keys()} return rating_dict - raise AssertionError(f"{item['imdb_id']} not found in page.ratings") + raise AssertionError() a_movie = { "title": "Kung Fu Panda 4", @@ -142,9 +128,8 @@ async def test_load_ratings_page_20240510(monkeypatch): "media_type": "Movie", "imdb_id": "tt21692408", "imdb_score": 59, - "imdb_votes": 36069, + "imdb_votes": 36000, "runtime": 94, - "genres": {"Action", "Adventure", "Animation"}, } assert a_movie == movie(a_movie) @@ -154,8 +139,7 @@ async def test_load_ratings_page_20240510(monkeypatch): "media_type": "TV Series", "imdb_id": "tt8888540", "imdb_score": 64, - "imdb_votes": 6044, - "genres": {"Drama"}, + "imdb_votes": 6000, } assert a_running_tvseries == movie(a_running_tvseries) @@ -165,94 +149,29 @@ async def test_load_ratings_page_20240510(monkeypatch): "media_type": "TV Series", "imdb_id": "tt0072500", "imdb_score": 87, - "imdb_votes": 100261, - "genres": {"Comedy"}, + "imdb_votes": 100000, } assert a_finished_tvseries == movie(a_finished_tvseries) a_tvepisode = { "title": "Columbo / No Time to Die", - "original_title": "Columbo / No Time to Die", + "original_title": None, "release_year": 1992, "media_type": "TV Episode", "imdb_id": "tt0103987", "imdb_score": 59, - "imdb_votes": 2122, + "imdb_votes": 2100, "runtime": 98, - "genres": {"Crime", "Drama", "Mystery"}, } assert a_tvepisode == movie(a_tvepisode) a_videogame = { "title": "Alan Wake", - "original_title": "Alan Wake", + "original_title": None, "release_year": 2010, "media_type": "Video Game", "imdb_id": "tt0466662", - # The data from __NEXT_DATA__ is wrong, the actual values should be: - # "imdb_score": 82, - # "imdb_votes": 7300, - # "genres": {"Action", "Adventure", "Horror"}, - "imdb_score": 67, # Wrong value, but correctly parsed from __NEXT_DATA__ - "imdb_votes": 11655, # Wrong value, but correctly parsed from __NEXT_DATA__ - "genres": {"Comedy", "Crime", "Drama"}, # Wrong value + "imdb_score": 82, + "imdb_votes": 7300, } assert a_videogame == movie(a_videogame) - - -@pytest.mark.asyncio -async def test_load_ratings_page_20240720(monkeypatch): - with bz2.open(fixturesdir / "ratings-ur655321-20240720.html.bz2", "rb") as f: - html = f.read() - soup = bs4.BeautifulSoup(html, "html5lib") - monkeypatch.setattr(imdb, "asoup_from_url", AsyncMock(return_value=soup)) - - with bz2.open(fixturesdir / "ratings-ur655321-20240720.gql.json.bz2", "rb") as f: - jsonstr = f.read() - async with imdb.asession() as s: - monkeypatch.setattr(s, "post", AsyncMock(return_value=_mock_response(jsonstr))) - page = await imdb._load_ratings_page("fakeurl", "ur655321") - assert len(page.ratings) == 100 - assert page.imdb_user_id is not None - assert page.imdb_user_id == "ur655321" - assert page.imdb_user_name == "AlexUltra" - assert page.next_page_url is None, "not supported for new ratings page" - - def movie(item: dict): - for rating in page.ratings: - assert rating.movie - if rating.movie.imdb_id == item["imdb_id"]: - rating_dict = {key: getattr(rating.movie, key) for key in item.keys()} - return rating_dict - raise AssertionError(f"{item['imdb_id']} not found in page.ratings") - - a_movie = { - "title": "Kung Fu Panda 4", - "release_year": 2024, - "media_type": "Movie", - "imdb_id": "tt21692408", - "imdb_score": 59, - "imdb_votes": 48018, - "runtime": 94, - } - assert a_movie == movie(a_movie) - - a_running_tvseries = { - "title": "Palm Royale", - "release_year": 2024, - "media_type": "TV Series", - "imdb_id": "tt8888540", - "imdb_score": 63, - "imdb_votes": 9458, - } - assert a_running_tvseries == movie(a_running_tvseries) - - a_finished_tvseries = { - "title": "Fawlty Towers", - "release_year": 1975, - "media_type": "TV Series", - "imdb_id": "tt0072500", - "imdb_score": 87, - "imdb_votes": 100860, - } - assert a_finished_tvseries == movie(a_finished_tvseries) diff --git a/tests/test_web.py b/tests/test_web.py index 05affd7..b1e7e4b 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -32,83 +32,6 @@ def admin_client() -> TestClient: return client -@pytest.mark.asyncio -async def test_get_ratings_for_group_with_awards( - conn: db.Connection, unauthorized_client: TestClient -): - user = models.User( - imdb_id="ur12345678", - name="user-1", - secret="secret-1", # noqa: S106 - groups=[], - ) - group = models.Group( - name="group-1", - users=[models.GroupUser(id=str(user.id), name=user.name)], - ) - user.groups = [models.UserGroup(id=str(group.id), access="r")] - path = app.url_path_for("get_ratings_for_group", group_id=str(group.id)) - - await db.add(conn, user) - await db.add(conn, group) - - movie1 = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt12345678", - genres={"genre-1"}, - ) - await db.add(conn, movie1) - movie2 = models.Movie( - title="test movie 2", - release_year=2014, - media_type="Movie", - imdb_id="tt12345679", - genres={"genre-2"}, - ) - await db.add(conn, movie2) - - award1 = models.Award( - movie_id=movie1.id, category="imdb-top-250", details='{"position":23}' - ) - award2 = models.Award( - movie_id=movie2.id, category="imdb-top-250", details='{"position":99}' - ) - award3 = models.Award( - movie_id=movie1.id, category="oscars", details='{"name":"Best Visual Effects"}' - ) - award4 = models.Award( - movie_id=movie1.id, category="oscars", details='{"name":"Best Picture"}' - ) - await db.add(conn, award1, award2, award3, award4) - - rating = models.Rating( - movie_id=movie1.id, user_id=user.id, score=66, rating_date=datetime.now(tz=UTC) - ) - await db.add(conn, rating) - - rating_aggregate = { - "canonical_title": movie1.title, - "imdb_score": movie1.imdb_score, - "imdb_votes": movie1.imdb_votes, - "link": imdb.movie_url(movie1.imdb_id), - "media_type": movie1.media_type, - "original_title": movie1.original_title, - "user_scores": [rating.score], - "year": movie1.release_year, - "awards": [ - "imdb-top-250:23", - "oscars:Best Picture", - "oscars:Best Visual Effects", - ], - } - - resp = unauthorized_client.get(path) - assert resp.status_code == 200 - assert resp.json() == [rating_aggregate] - - @pytest.mark.asyncio async def test_get_ratings_for_group( conn: db.Connection, unauthorized_client: TestClient @@ -159,7 +82,6 @@ async def test_get_ratings_for_group( "original_title": movie.original_title, "user_scores": [rating.score], "year": movie.release_year, - "awards": [], } resp = unauthorized_client.get(path) @@ -236,7 +158,6 @@ async def test_list_movies( "original_title": m.original_title, "user_scores": [], "year": m.release_year, - "awards": [], } response = authorized_client.get(path, params={"imdb_id": m.imdb_id}) diff --git a/unwind-ui/package-lock.json b/unwind-ui/package-lock.json index 482df58..d01fcf0 100644 --- a/unwind-ui/package-lock.json +++ b/unwind-ui/package-lock.json @@ -15,36 +15,14 @@ "@vue/compiler-sfc": "^3.0.5", "prettier": "^3.2.5", "typescript": "^5.4.5", - "vite": "^6.3.5", + "vite": "^5.2.11", "vue-tsc": "^2.0.16" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -52,856 +30,693 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "cpu": [ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "cpu": [ "mips64el" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "dev": true, - "license": "MIT" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "node_modules/@vitejs/plugin-vue": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", - "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz", + "integrity": "sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ==", "dev": true, - "license": "MIT", "engines": { "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "vite": "^5.0.0 || ^6.0.0", + "vite": "^5.0.0", "vue": "^3.2.25" } }, "node_modules/@volar/language-core": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.13.tgz", - "integrity": "sha512-MnQJ7eKchJx5Oz+YdbqyFUk8BN6jasdJv31n/7r6/WwlOOv7qzvot6B66887l2ST3bUW4Mewml54euzpJWA6bg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.1.tgz", + "integrity": "sha512-iHJAZKcYldZgyS8gx6DfIZApViVBeqbf6iPhqoZpG5A6F4zsZiFldKfwaKaBA3/wnOTWE2i8VUbXywI1WywCPg==", "dev": true, - "license": "MIT", "dependencies": { - "@volar/source-map": "2.4.13" + "@volar/source-map": "2.2.1" } }, "node_modules/@volar/source-map": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.13.tgz", - "integrity": "sha512-l/EBcc2FkvHgz2ZxV+OZK3kMSroMr7nN3sZLF2/f6kWW66q8+tEL4giiYyFjt0BcubqJhBt6soYIrAPhg/Yr+Q==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.1.tgz", + "integrity": "sha512-w1Bgpguhbp7YTr7VUFu6gb4iAZjeEPsOX4zpgiuvlldbzvIWDWy4t0jVifsIsxZ99HAu+c3swiME7wt+GeNqhA==", "dev": true, - "license": "MIT" + "dependencies": { + "muggle-string": "^0.4.0" + } }, "node_modules/@volar/typescript": { - "version": "2.4.13", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.13.tgz", - "integrity": "sha512-Ukz4xv84swJPupZeoFsQoeJEOm7U9pqsEnaGGgt5ni3SCTa22m8oJP5Nng3Wed7Uw5RBELdLxxORX8YhJPyOgQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.1.tgz", + "integrity": "sha512-Z/tqluR7Hz5/5dCqQp7wo9C/6tSv/IYl+tTzgzUt2NjTq95bKSsuO4E+V06D0c+3aP9x5S9jggLqw451hpnc6Q==", "dev": true, - "license": "MIT", "dependencies": { - "@volar/language-core": "2.4.13", - "path-browserify": "^1.0.1", - "vscode-uri": "^3.0.8" + "@volar/language-core": "2.2.1", + "path-browserify": "^1.0.1" } }, "node_modules/@vue/compiler-core": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.14.tgz", - "integrity": "sha512-k7qMHMbKvoCXIxPhquKQVw3Twid3Kg4s7+oYURxLGRd56LiuHJVrvFKI4fm2AM3c8apqODPfVJGoh8nePbXMRA==", - "license": "MIT", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.27.tgz", + "integrity": "sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==", "dependencies": { - "@babel/parser": "^7.27.2", - "@vue/shared": "3.5.14", + "@babel/parser": "^7.24.4", + "@vue/shared": "3.4.27", "entities": "^4.5.0", "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.14.tgz", - "integrity": "sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==", - "license": "MIT", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.27.tgz", + "integrity": "sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==", "dependencies": { - "@vue/compiler-core": "3.5.14", - "@vue/shared": "3.5.14" + "@vue/compiler-core": "3.4.27", + "@vue/shared": "3.4.27" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.14.tgz", - "integrity": "sha512-9T6m/9mMr81Lj58JpzsiSIjBgv2LiVoWjIVa7kuXHICUi8LiDSIotMpPRXYJsXKqyARrzjT24NAwttrMnMaCXA==", - "license": "MIT", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.27.tgz", + "integrity": "sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==", "dependencies": { - "@babel/parser": "^7.27.2", - "@vue/compiler-core": "3.5.14", - "@vue/compiler-dom": "3.5.14", - "@vue/compiler-ssr": "3.5.14", - "@vue/shared": "3.5.14", + "@babel/parser": "^7.24.4", + "@vue/compiler-core": "3.4.27", + "@vue/compiler-dom": "3.4.27", + "@vue/compiler-ssr": "3.4.27", + "@vue/shared": "3.4.27", "estree-walker": "^2.0.2", - "magic-string": "^0.30.17", - "postcss": "^8.5.3", - "source-map-js": "^1.2.1" + "magic-string": "^0.30.10", + "postcss": "^8.4.38", + "source-map-js": "^1.2.0" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.14.tgz", - "integrity": "sha512-Y0G7PcBxr1yllnHuS/NxNCSPWnRGH4Ogrp0tsLA5QemDZuJLs99YjAKQ7KqkHE0vCg4QTKlQzXLKCMF7WPSl7Q==", - "license": "MIT", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.27.tgz", + "integrity": "sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==", "dependencies": { - "@vue/compiler-dom": "3.5.14", - "@vue/shared": "3.5.14" - } - }, - "node_modules/@vue/compiler-vue2": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", - "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", - "dev": true, - "license": "MIT", - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" + "@vue/compiler-dom": "3.4.27", + "@vue/shared": "3.4.27" } }, "node_modules/@vue/language-core": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.10.tgz", - "integrity": "sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==", + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.16.tgz", + "integrity": "sha512-Bc2sexRH99pznOph8mLw2BlRZ9edm7tW51kcBXgx8adAoOcZUWJj3UNSsdQ6H9Y8meGz7BoazVrVo/jUukIsPw==", "dev": true, - "license": "MIT", "dependencies": { - "@volar/language-core": "~2.4.11", - "@vue/compiler-dom": "^3.5.0", - "@vue/compiler-vue2": "^2.7.16", - "@vue/shared": "^3.5.0", - "alien-signals": "^1.0.3", + "@volar/language-core": "~2.2.0", + "@vue/compiler-dom": "^3.4.0", + "@vue/shared": "^3.4.0", + "computeds": "^0.0.1", "minimatch": "^9.0.3", - "muggle-string": "^0.4.1", - "path-browserify": "^1.0.1" + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" }, "peerDependencies": { "typescript": "*" @@ -913,103 +728,150 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.14.tgz", - "integrity": "sha512-7cK1Hp343Fu/SUCCO52vCabjvsYu7ZkOqyYu7bXV9P2yyfjUMUXHZafEbq244sP7gf+EZEz+77QixBTuEqkQQw==", - "license": "MIT", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.27.tgz", + "integrity": "sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==", "dependencies": { - "@vue/shared": "3.5.14" + "@vue/shared": "3.4.27" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.14.tgz", - "integrity": "sha512-w9JWEANwHXNgieAhxPpEpJa+0V5G0hz3NmjAZwlOebtfKyp2hKxKF0+qSh0Xs6/PhfGihuSdqMprMVcQU/E6ag==", - "license": "MIT", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.27.tgz", + "integrity": "sha512-7aYA9GEbOOdviqVvcuweTLe5Za4qBZkUY7SvET6vE8kyypxVgaT1ixHLg4urtOlrApdgcdgHoTZCUuTGap/5WA==", "dependencies": { - "@vue/reactivity": "3.5.14", - "@vue/shared": "3.5.14" + "@vue/reactivity": "3.4.27", + "@vue/shared": "3.4.27" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.14.tgz", - "integrity": "sha512-lCfR++IakeI35TVR80QgOelsUIdcKjd65rWAMfdSlCYnaEY5t3hYwru7vvcWaqmrK+LpI7ZDDYiGU5V3xjMacw==", - "license": "MIT", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.27.tgz", + "integrity": "sha512-ScOmP70/3NPM+TW9hvVAz6VWWtZJqkbdf7w6ySsws+EsqtHvkhxaWLecrTorFxsawelM5Ys9FnDEMt6BPBDS0Q==", "dependencies": { - "@vue/reactivity": "3.5.14", - "@vue/runtime-core": "3.5.14", - "@vue/shared": "3.5.14", + "@vue/runtime-core": "3.4.27", + "@vue/shared": "3.4.27", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.14.tgz", - "integrity": "sha512-Rf/ISLqokIvcySIYnv3tNWq40PLpNLDLSJwwVWzG6MNtyIhfbcrAxo5ZL9nARJhqjZyWWa40oRb2IDuejeuv6w==", - "license": "MIT", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.27.tgz", + "integrity": "sha512-dlAMEuvmeA3rJsOMJ2J1kXU7o7pOxgsNHVr9K8hB3ImIkSuBrIdy0vF66h8gf8Tuinf1TK3mPAz2+2sqyf3KzA==", "dependencies": { - "@vue/compiler-ssr": "3.5.14", - "@vue/shared": "3.5.14" + "@vue/compiler-ssr": "3.4.27", + "@vue/shared": "3.4.27" }, "peerDependencies": { - "vue": "3.5.14" + "vue": "3.4.27" } }, "node_modules/@vue/shared": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.14.tgz", - "integrity": "sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==", - "license": "MIT" + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.27.tgz", + "integrity": "sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==" }, - "node_modules/alien-signals": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", - "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", - "dev": true, - "license": "MIT" + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/bulma": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.4.tgz", - "integrity": "sha512-Ffb6YGXDiZYX3cqvSbHWqQ8+LkX6tVoTcZuVB3lm93sbAVXlO0D6QlOTMnV6g18gILpAXqkG2z9hf9z4hCjz2g==", - "license": "MIT" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.0.tgz", + "integrity": "sha512-7n49v/gdHXaHcU9fVobqGXO2OguiCoMh6CLbeX7jq00XrZ5vOSE4LNS0S/0Q6rlBbckY6kk6W7LwqxS0nu4bug==", + "dependencies": { + "sass": "^1.71.1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/de-indent": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -1018,74 +880,64 @@ } }, "node_modules/esbuild": { - "version": "0.25.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", - "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=18" + "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.4", - "@esbuild/android-arm": "0.25.4", - "@esbuild/android-arm64": "0.25.4", - "@esbuild/android-x64": "0.25.4", - "@esbuild/darwin-arm64": "0.25.4", - "@esbuild/darwin-x64": "0.25.4", - "@esbuild/freebsd-arm64": "0.25.4", - "@esbuild/freebsd-x64": "0.25.4", - "@esbuild/linux-arm": "0.25.4", - "@esbuild/linux-arm64": "0.25.4", - "@esbuild/linux-ia32": "0.25.4", - "@esbuild/linux-loong64": "0.25.4", - "@esbuild/linux-mips64el": "0.25.4", - "@esbuild/linux-ppc64": "0.25.4", - "@esbuild/linux-riscv64": "0.25.4", - "@esbuild/linux-s390x": "0.25.4", - "@esbuild/linux-x64": "0.25.4", - "@esbuild/netbsd-arm64": "0.25.4", - "@esbuild/netbsd-x64": "0.25.4", - "@esbuild/openbsd-arm64": "0.25.4", - "@esbuild/openbsd-x64": "0.25.4", - "@esbuild/sunos-x64": "0.25.4", - "@esbuild/win32-arm64": "0.25.4", - "@esbuild/win32-ia32": "0.25.4", - "@esbuild/win32-x64": "0.25.4" + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" } }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, - "node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": ">=8" } }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1094,31 +946,82 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, - "license": "MIT", "bin": { "he": "bin/he" } }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", + "node_modules/immutable": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz", + "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -1133,20 +1036,18 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1154,36 +1055,40 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -1198,22 +1103,20 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, - "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -1224,14 +1127,24 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/rollup": { - "version": "4.40.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", - "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", - "dev": true, - "license": "MIT", + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dependencies": { - "@types/estree": "1.0.7" + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rollup": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" }, "bin": { "rollup": "dist/bin/rollup" @@ -1241,61 +1154,77 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.2", - "@rollup/rollup-android-arm64": "4.40.2", - "@rollup/rollup-darwin-arm64": "4.40.2", - "@rollup/rollup-darwin-x64": "4.40.2", - "@rollup/rollup-freebsd-arm64": "4.40.2", - "@rollup/rollup-freebsd-x64": "4.40.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", - "@rollup/rollup-linux-arm-musleabihf": "4.40.2", - "@rollup/rollup-linux-arm64-gnu": "4.40.2", - "@rollup/rollup-linux-arm64-musl": "4.40.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-gnu": "4.40.2", - "@rollup/rollup-linux-riscv64-musl": "4.40.2", - "@rollup/rollup-linux-s390x-gnu": "4.40.2", - "@rollup/rollup-linux-x64-gnu": "4.40.2", - "@rollup/rollup-linux-x64-musl": "4.40.2", - "@rollup/rollup-win32-arm64-msvc": "4.40.2", - "@rollup/rollup-win32-ia32-msvc": "4.40.2", - "@rollup/rollup-win32-x64-msvc": "4.40.2", + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", "fsevents": "~2.3.2" } }, + "node_modules/sass": { + "version": "1.77.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.0.tgz", + "integrity": "sha512-eGj4HNfXqBWtSnvItNkn7B6icqH14i3CiCGbzMKs3BAPTq62pp9NBYsBgyN4cA+qssqo9r26lW4JSvlaUUWbgw==", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/semver": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.1.tgz", + "integrity": "sha512-f/vbBsu+fOiYt+lmwZV0rVwJScl46HppnOA1ZvIuBWKOTlllpyJ3bfVax76/OrhCH38dyxoDIA8K7uB963IYgA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } }, - "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", - "dev": true, - "license": "MIT", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "is-number": "^7.0.0" }, "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=8.0" } }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "devOptional": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1305,24 +1234,20 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", "dev": true, - "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^18.0.0 || >=20.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -1331,25 +1256,18 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", + "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", - "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" + "terser": "^5.4.0" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "jiti": { - "optional": true - }, "less": { "optional": true }, @@ -1359,9 +1277,6 @@ "sass": { "optional": true }, - "sass-embedded": { - "optional": true - }, "stylus": { "optional": true }, @@ -1370,33 +1285,19 @@ }, "terser": { "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true } } }, - "node_modules/vscode-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", - "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", - "dev": true, - "license": "MIT" - }, "node_modules/vue": { - "version": "3.5.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.14.tgz", - "integrity": "sha512-LbOm50/vZFG6Mhy6KscQYXZMQ0LMCC/y40HDJPPvGFQ+i/lUH+PJHR6C3assgOQiXdl6tAfsXHbXYVBZZu65ew==", - "license": "MIT", + "version": "3.4.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz", + "integrity": "sha512-8s/56uK6r01r1icG/aEOHqyMVxd1bkYcSe9j8HcKtr/xTOFWvnzIVTehNW+5Yt89f+DLBe4A569pnZLS5HzAMA==", "dependencies": { - "@vue/compiler-dom": "3.5.14", - "@vue/compiler-sfc": "3.5.14", - "@vue/runtime-dom": "3.5.14", - "@vue/server-renderer": "3.5.14", - "@vue/shared": "3.5.14" + "@vue/compiler-dom": "3.4.27", + "@vue/compiler-sfc": "3.4.27", + "@vue/runtime-dom": "3.4.27", + "@vue/server-renderer": "3.4.27", + "@vue/shared": "3.4.27" }, "peerDependencies": { "typescript": "*" @@ -1407,21 +1308,31 @@ } } }, - "node_modules/vue-tsc": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.10.tgz", - "integrity": "sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==", + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", "dev": true, - "license": "MIT", "dependencies": { - "@volar/typescript": "~2.4.11", - "@vue/language-core": "2.2.10" + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.16.tgz", + "integrity": "sha512-/gHAWJa216PeEhfxtAToIbxdWgw01wuQzo48ZUqMYVEyNqDp+OYV9xMO5HaPS2P3Ls0+EsjguMZLY4cGobX4Ew==", + "dev": true, + "dependencies": { + "@volar/typescript": "~2.2.0", + "@vue/language-core": "2.0.16", + "semver": "^7.5.4" }, "bin": { "vue-tsc": "bin/vue-tsc.js" }, "peerDependencies": { - "typescript": ">=5.0.0" + "typescript": "*" } } } diff --git a/unwind-ui/package.json b/unwind-ui/package.json index a87ca11..c49115b 100644 --- a/unwind-ui/package.json +++ b/unwind-ui/package.json @@ -16,7 +16,7 @@ "@vue/compiler-sfc": "^3.0.5", "prettier": "^3.2.5", "typescript": "^5.4.5", - "vite": "^6.3.5", + "vite": "^5.2.11", "vue-tsc": "^2.0.16" }, "prettier": { diff --git a/unwind/__main__.py b/unwind/__main__.py index a193d8e..82abfee 100644 --- a/unwind/__main__.py +++ b/unwind/__main__.py @@ -1,26 +1,149 @@ import argparse import asyncio import logging -import sys +import secrets +from base64 import b64encode +from pathlib import Path -from . import cli, config +from . import config, db, models, utils +from .db import close_connection_pool, open_connection_pool +from .imdb import refresh_user_ratings_from_imdb +from .imdb_import import download_datasets, import_from_file -log = logging.getLogger(__package__) +log = logging.getLogger(__name__) + + +async def run_add_user(user_id: str, name: str, overwrite_existing: bool): + if not user_id.startswith("ur"): + raise ValueError(f"Invalid IMDb user ID: {user_id!a}") + + await open_connection_pool() + + async with db.new_connection() as conn: + user = await db.get(conn, models.User, imdb_id=user_id) + + if user is not None: + if overwrite_existing: + log.warning("⚠️ Overwriting existing user: %a", user) + else: + log.error("❌ User already exists: %a", user) + return + + secret = secrets.token_bytes() + + user = models.User(name=name, imdb_id=user_id, secret=utils.phc_scrypt(secret)) + async with db.transaction() as conn: + await db.add_or_update_user(conn, user) + + user_data = { + "secret": b64encode(secret), + "user": models.asplain(user), + } + + log.info("✨ User created: %a", user_data) + + await close_connection_pool() + + +async def run_load_user_ratings_from_imdb(): + await open_connection_pool() + + i = 0 + async for _ in refresh_user_ratings_from_imdb(): + i += 1 + + log.info("✨ Imported %s new ratings.", i) + + await close_connection_pool() + + +async def run_import_imdb_dataset(basics_path: Path, ratings_path: Path): + await open_connection_pool() + + await import_from_file(basics_path=basics_path, ratings_path=ratings_path) + + await close_connection_pool() + + +async def run_download_imdb_dataset(basics_path: Path, ratings_path: Path): + await download_datasets(basics_path=basics_path, ratings_path=ratings_path) def getargs(): - parser = argparse.ArgumentParser(prog="unwind", allow_abbrev=False) - commands = parser.add_subparsers(title="commands", metavar="COMMAND", dest="mode") + parser = argparse.ArgumentParser() + commands = parser.add_subparsers(required=True) - for module in cli.modules: - help_, *descr = module.help.splitlines() - cmd = commands.add_parser( - module.name, - help=help_, - description="\n".join(descr) or help_, - allow_abbrev=False, - ) - module.add_args(cmd) + parser_import_imdb_dataset = commands.add_parser( + "import-imdb-dataset", + help="Import IMDb datasets.", + description=""" + Import IMDb datasets. + New datasets available from https://www.imdb.com/interfaces/. + """, + ) + parser_import_imdb_dataset.add_argument( + dest="mode", + action="store_const", + const="import-imdb-dataset", + ) + parser_import_imdb_dataset.add_argument( + "--basics", metavar="basics_file.tsv.gz", type=Path, required=True + ) + parser_import_imdb_dataset.add_argument( + "--ratings", metavar="ratings_file.tsv.gz", type=Path, required=True + ) + + parser_download_imdb_dataset = commands.add_parser( + "download-imdb-dataset", + help="Download IMDb datasets.", + description=""" + Download IMDb datasets. + """, + ) + parser_download_imdb_dataset.add_argument( + dest="mode", + action="store_const", + const="download-imdb-dataset", + ) + parser_download_imdb_dataset.add_argument( + "--basics", metavar="basics_file.tsv.gz", type=Path, required=True + ) + parser_download_imdb_dataset.add_argument( + "--ratings", metavar="ratings_file.tsv.gz", type=Path, required=True + ) + + parser_load_user_ratings_from_imdb = commands.add_parser( + "load-user-ratings-from-imdb", + help="Load user ratings from imdb.com.", + description=""" + Refresh user ratings for all registered users live from IMDb's website. + """, + ) + parser_load_user_ratings_from_imdb.add_argument( + dest="mode", + action="store_const", + const="load-user-ratings-from-imdb", + ) + + parser_add_user = commands.add_parser( + "add-user", + help="Add a new user.", + description=""" + Add a new user. + """, + ) + parser_add_user.add_argument( + dest="mode", + action="store_const", + const="add-user", + ) + parser_add_user.add_argument("--name", required=True) + parser_add_user.add_argument("--imdb-id", required=True) + parser_add_user.add_argument( + "--overwrite-existing", + action="store_true", + help="Allow overwriting an existing user. WARNING: This will reset the user's password!", + ) try: args = parser.parse_args() @@ -28,10 +151,6 @@ def getargs(): parser.print_usage() raise - if args.mode is None: - parser.print_help() - sys.exit(1) - return args @@ -39,16 +158,23 @@ def main(): logging.basicConfig( format="%(asctime)s.%(msecs)03d [%(name)s:%(process)d] %(levelname)s: %(message)s", datefmt="%H:%M:%S", - # level=config.loglevel, + level=config.loglevel, ) - log.setLevel(config.loglevel) log.debug(f"Log level: {config.loglevel}") - args = getargs() + try: + args = getargs() + except Exception: + return - modes = {m.name: m.main for m in cli.modules} - if handler := modes.get(args.mode): - asyncio.run(handler(args)) + if args.mode == "load-user-ratings-from-imdb": + asyncio.run(run_load_user_ratings_from_imdb()) + elif args.mode == "add-user": + asyncio.run(run_add_user(args.imdb_id, args.name, args.overwrite_existing)) + elif args.mode == "import-imdb-dataset": + asyncio.run(run_import_imdb_dataset(args.basics, args.ratings)) + elif args.mode == "download-imdb-dataset": + asyncio.run(run_download_imdb_dataset(args.basics, args.ratings)) main() diff --git a/unwind/cli/__init__.py b/unwind/cli/__init__.py deleted file mode 100644 index dd6f8cc..0000000 --- a/unwind/cli/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -import argparse -import importlib -from pathlib import Path -from types import ModuleType -from typing import Any, Callable, Coroutine, Iterable, Protocol, TypeGuard - -type CommandHandler = Callable[[argparse.Namespace], Coroutine[Any, Any, None]] - - -class CliModule(Protocol): - name: str - help: str - add_args: Callable[[argparse.ArgumentParser], None] - main: CommandHandler - - -def _is_cli_module(m: ModuleType) -> TypeGuard[CliModule]: - return ( - hasattr(m, "name") - and hasattr(m, "help") - and hasattr(m, "add_args") - and hasattr(m, "main") - ) - - -_clidir = Path(__file__).parent - - -def _load_cmds() -> Iterable[CliModule]: - """Return all CLI command modules.""" - for f in _clidir.iterdir(): - if f.suffix == ".py" and not f.name.startswith("__"): - m = importlib.import_module(f"{__package__}.{f.stem}") - if not _is_cli_module(m): - raise ValueError(f"Invalid CLI module: {m!a}") - yield m - - -modules = sorted(_load_cmds(), key=lambda m: m.name) diff --git a/unwind/cli/add_user.py b/unwind/cli/add_user.py deleted file mode 100644 index cc3d305..0000000 --- a/unwind/cli/add_user.py +++ /dev/null @@ -1,56 +0,0 @@ -import argparse -import logging -import secrets - -from unwind import db, models, utils - -log = logging.getLogger(__name__) - -name = "add-user" -help = "Add a new user." - - -def add_args(cmd: argparse.ArgumentParser) -> None: - cmd.add_argument("--name", required=True) - cmd.add_argument("--imdb-id", required=True) - cmd.add_argument( - "--overwrite-existing", - action="store_true", - help="Allow overwriting an existing user. WARNING: This will reset the user's password!", - ) - - -async def main(args: argparse.Namespace) -> None: - user_id: str = args.imdb_id - name: str = args.name - overwrite_existing: bool = args.overwrite_existing - - if not user_id.startswith("ur"): - raise ValueError(f"Invalid IMDb user ID: {user_id!a}") - - await db.open_connection_pool() - - async with db.new_connection() as conn: - user = await db.get(conn, models.User, imdb_id=user_id) - - if user is not None: - if overwrite_existing: - log.warning("⚠️ Overwriting existing user: %a", user) - else: - log.error("❌ User already exists: %a", user) - return - - secret = secrets.token_bytes() - - user = models.User(name=name, imdb_id=user_id, secret=utils.phc_scrypt(secret)) - async with db.transaction() as conn: - await db.add_or_update_user(conn, user) - - user_data = { - "secret": utils.b64encode(secret), - "user": models.asplain(user), - } - - log.info("✨ User created: %a", user_data) - - await db.close_connection_pool() diff --git a/unwind/cli/download_imdb_dataset.py b/unwind/cli/download_imdb_dataset.py deleted file mode 100644 index 5b81045..0000000 --- a/unwind/cli/download_imdb_dataset.py +++ /dev/null @@ -1,24 +0,0 @@ -import argparse -import logging -from pathlib import Path - -from unwind.imdb_import import download_datasets - -log = logging.getLogger(__name__) - -name = "download-imdb-dataset" -help = "Download IMDb datasets." - - -def add_args(cmd: argparse.ArgumentParser) -> None: - cmd.add_argument("--basics", metavar="basics_file.tsv.gz", type=Path, required=True) - cmd.add_argument( - "--ratings", metavar="ratings_file.tsv.gz", type=Path, required=True - ) - - -async def main(args: argparse.Namespace) -> None: - basics_path: Path = args.basics - ratings_path: Path = args.ratings - - await download_datasets(basics_path=basics_path, ratings_path=ratings_path) diff --git a/unwind/cli/import_imdb_dataset.py b/unwind/cli/import_imdb_dataset.py deleted file mode 100644 index 3adb5da..0000000 --- a/unwind/cli/import_imdb_dataset.py +++ /dev/null @@ -1,31 +0,0 @@ -import argparse -import logging -from pathlib import Path - -from unwind import db -from unwind.imdb_import import import_from_file - -log = logging.getLogger(__name__) - -name = "import-imdb-dataset" -help = """Import IMDb datasets. -New datasets available from https://www.imdb.com/interfaces/. -""" - - -def add_args(cmd: argparse.ArgumentParser) -> None: - cmd.add_argument("--basics", metavar="basics_file.tsv.gz", type=Path, required=True) - cmd.add_argument( - "--ratings", metavar="ratings_file.tsv.gz", type=Path, required=True - ) - - -async def main(args: argparse.Namespace) -> None: - basics_path: Path = args.basics - ratings_path: Path = args.ratings - - await db.open_connection_pool() - - await import_from_file(basics_path=basics_path, ratings_path=ratings_path) - - await db.close_connection_pool() diff --git a/unwind/cli/import_wikidata_oscars.py b/unwind/cli/import_wikidata_oscars.py deleted file mode 100644 index cafc341..0000000 --- a/unwind/cli/import_wikidata_oscars.py +++ /dev/null @@ -1,102 +0,0 @@ -import argparse -import json -import logging -from datetime import datetime -from pathlib import Path -from typing import Iterable - -from unwind import db, models, types - -log = logging.getLogger(__name__) - -name = "import-wikidata-oscars" -help = "Import Academy awards information from a Wikidata dump." - -# To generate the JSON file, run the following query -# at https://query.wikidata.org/ and export as (simpel) JSON: -""" -SELECT ?awardLabel ?filmLabel ?imdbId ?time WHERE { - ?award wdt:P31 wd:Q19020. - ?film wdt:P31 wd:Q11424; - p:P166 ?awardStat. - ?awardStat ps:P166 ?award. - OPTIONAL { - ?awardStat pq:P805 ?awardEdition. - ?awardEdition wdt:P585 ?time. - ?film wdt:P345 ?imdbId. - } - SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE],en". } -} -ORDER BY DESC (?time) -""" - - -def add_args(cmd: argparse.ArgumentParser) -> None: - cmd.add_argument("--json-file", required=True, type=Path) - - -def load_awards(json_file: Path) -> Iterable[tuple[types.ImdbMovieId, models.Award]]: - with json_file.open() as fd: - data = json.load(fd) - - name_prefix = "Academy Award for " - special_names = { - "Special Achievement Academy Award": "Special Achievement", - "Academy Honorary Award": "Honorary", - } - for item in data: - name = item["awardLabel"] - if name in special_names: - name = special_names[name] - elif name.startswith(name_prefix): - name = name.removeprefix(name_prefix) - else: - raise ValueError(f"Award name is unexpected: {name!a}") - # award = models.Award(category="oscars",details={"name":name},created=created) - award = models.Award(category="oscars") - # award._details = {"name": name} - award.name = name - if (datestr := item.get("time")) is not None: - award.created = datetime.fromisoformat(datestr) - - if "imdbId" not in item: - log.warning("⚠️ IMDb ID missing for movie: %a", item["filmLabel"]) - else: - yield item["imdbId"], award - - -async def remove_all_oscars(conn: db.Connection) -> None: - stmt = models.awards.delete().where(models.awards.c.category == "oscars") - await conn.execute(stmt) - - -async def main(args: argparse.Namespace) -> None: - await db.open_connection_pool() - - json_file: Path = args.json_file - - awards: dict[types.ImdbMovieId, list[models.Award]] = {} - for imdb_id, award in load_awards(json_file): - awards.setdefault(imdb_id, []).append(award) - async with db.new_connection() as conn: - imdb_ids = list(awards) - available = await db.get_movie_ids(conn, imdb_ids) - if missing := set(imdb_ids).difference(available): - log.warning( - "⚠️ File (%a) contained %i unknown movies: %a", - str(json_file), - len(missing), - missing, - ) - - async with db.transaction() as conn: - await remove_all_oscars(conn) - - for imdb_id, unwind_id in available.items(): - for award in awards[imdb_id]: - award.movie_id = unwind_id - await db.add(conn, award) - - log.info(f"✨ Imported {len(available)} oscars.") - - await db.close_connection_pool() diff --git a/unwind/cli/load_imdb_charts.py b/unwind/cli/load_imdb_charts.py deleted file mode 100644 index 3d6d7b5..0000000 --- a/unwind/cli/load_imdb_charts.py +++ /dev/null @@ -1,86 +0,0 @@ -import argparse -import logging -from typing import Callable - -from unwind import db, imdb, models - -log = logging.getLogger(__name__) - -name = "load-imdb-charts" -help = "Load and import charts from imdb.com." - - -def add_args(cmd: argparse.ArgumentParser) -> None: - cmd.add_argument( - "--select", - action="append", - dest="charts", - default=[], - choices={"top250", "bottom100", "pop100"}, - help="Select which charts to refresh.", - ) - - -async def remove_all_awards( - conn: db.Connection, category: models.AwardCategory -) -> None: - stmt = models.awards.delete().where(models.awards.c.category == category) - await conn.execute(stmt) - - -_award_handlers: dict[models.AwardCategory, Callable] = { - "imdb-pop-100": imdb.load_most_popular_100, - "imdb-top-250": imdb.load_top_250, - "imdb-bottom-100": imdb.load_bottom_100, -} - - -async def update_awards(conn: db.Connection, category: models.AwardCategory) -> None: - load_imdb_ids = _award_handlers[category] - imdb_ids = await load_imdb_ids() - - available = await db.get_movie_ids(conn, imdb_ids) - if missing := set(imdb_ids).difference(available): - log.warning( - "⚠️ Charts for category (%a) contained %i unknown movies: %a", - category, - len(missing), - missing, - ) - - await remove_all_awards(conn, category=category) - - for pos, imdb_id in enumerate(imdb_ids, 1): - if (movie_id := available.get(imdb_id)) is None: - continue - - award = models.Award( - movie_id=movie_id, - category=category, - ) - award.position = pos - await db.add(conn, award) - - -async def main(args: argparse.Namespace) -> None: - await db.open_connection_pool() - - if not args.charts: - args.charts = {"top250", "bottom100", "pop100"} - - if "pop100" in args.charts: - async with db.transaction() as conn: - await update_awards(conn, "imdb-pop-100") - log.info("✨ Updated most popular 100 movies.") - - if "bottom100" in args.charts: - async with db.transaction() as conn: - await update_awards(conn, "imdb-bottom-100") - log.info("✨ Updated bottom 100 movies.") - - if "top250" in args.charts: - async with db.transaction() as conn: - await update_awards(conn, "imdb-top-250") - log.info("✨ Updated top 250 rated movies.") - - await db.close_connection_pool() diff --git a/unwind/cli/load_user_ratings_from_imdb.py b/unwind/cli/load_user_ratings_from_imdb.py deleted file mode 100644 index b4a8e0f..0000000 --- a/unwind/cli/load_user_ratings_from_imdb.py +++ /dev/null @@ -1,28 +0,0 @@ -import argparse -import logging - -from unwind import db -from unwind.imdb import refresh_user_ratings_from_imdb - -log = logging.getLogger(__name__) - -name = "load-user-ratings-from-imdb" -help = """Load user ratings from imdb.com. -Refresh user ratings for all registered users live from IMDb's website. -""" - - -def add_args(cmd: argparse.ArgumentParser) -> None: - pass - - -async def main(args: argparse.Namespace) -> None: - await db.open_connection_pool() - - i = 0 - async for _ in refresh_user_ratings_from_imdb(): - i += 1 - - log.info("✨ Imported %s new ratings.", i) - - await db.close_connection_pool() diff --git a/unwind/db.py b/unwind/db.py index 664a629..7759f1b 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -1,25 +1,21 @@ import contextlib import logging from pathlib import Path -from typing import Any, AsyncGenerator, Iterable, Literal, Sequence, Type +from typing import Any, AsyncGenerator, Iterable, Literal, Sequence, Type, TypeVar import sqlalchemy as sa +from sqlalchemy.dialects.sqlite import insert from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine -import alembic.command -import alembic.config -import alembic.migration - from . import config from .models import ( - Award, Model, Movie, Progress, Rating, User, asplain, - awards, + db_patches, fromplain, metadata, movies, @@ -28,33 +24,15 @@ from .models import ( ratings, utcnow, ) -from .types import ULID, ImdbMovieId, MovieId, UserIdStr +from .types import ULID log = logging.getLogger(__name__) +T = TypeVar("T") _engine: AsyncEngine | None = None type Connection = AsyncConnection -_project_dir = Path(__file__).parent.parent -_alembic_ini = _project_dir / "alembic.ini" - - -def _init(conn: sa.Connection) -> None: - # See https://alembic.sqlalchemy.org/en/latest/cookbook.html#building-an-up-to-date-database-from-scratch - context = alembic.migration.MigrationContext.configure(conn) - heads = context.get_current_heads() - - is_empty_db = not heads # We consider a DB empty if Alembic hasn't touched it yet. - if is_empty_db: - log.info("⚡️ Initializing empty database.") - metadata.create_all(conn) - - # We pass our existing connection to Alembic's env.py, to avoid running another asyncio loop there. - alembic_cfg = alembic.config.Config(_alembic_ini) - alembic_cfg.attributes["connection"] = conn - alembic.command.stamp(alembic_cfg, "head") - async def open_connection_pool() -> None: """Open the DB connection pool. @@ -63,7 +41,11 @@ async def open_connection_pool() -> None: """ async with transaction() as conn: await conn.execute(sa.text("PRAGMA journal_mode=WAL")) - await conn.run_sync(_init) + + await conn.run_sync(metadata.create_all, tables=[db_patches]) + + async with new_connection() as conn: + await apply_db_patches(conn) async def close_connection_pool() -> None: @@ -83,7 +65,65 @@ async def close_connection_pool() -> None: await engine.dispose() -async def vacuum(conn: Connection, /) -> None: +async def current_patch_level(conn: Connection, /) -> str: + query = sa.select(db_patches.c.current) + current = await conn.scalar(query) + return current or "" + + +async def set_current_patch_level(conn: Connection, /, current: str) -> None: + stmt = insert(db_patches).values(id=1, current=current) + stmt = stmt.on_conflict_do_update(set_={"current": stmt.excluded.current}) + await conn.execute(stmt) + + +db_patches_dir = Path(__file__).parent / "sql" + + +async def apply_db_patches(conn: Connection, /) -> None: + """Apply all remaining patches to the database. + + Beware that patches will be applied in lexicographical order, + i.e. "10" comes before "9". + + The current patch state is recorded in the DB itself. + + Please note that every SQL statement in a patch file MUST be terminated + using two consecutive semi-colons (;). + Failing to do so will result in an error. + """ + applied_lvl = await current_patch_level(conn) + + did_patch = False + + for patchfile in sorted(db_patches_dir.glob("*.sql"), key=lambda p: p.stem): + patch_lvl = patchfile.stem + if patch_lvl <= applied_lvl: + continue + + log.info("Applying patch: %s", patch_lvl) + + sql = patchfile.read_text() + queries = sql.split(";;") + if len(queries) < 2: + log.error( + "Patch file is missing statement terminator (`;;'): %s", patchfile + ) + raise RuntimeError("No statement found.") + + async with transacted(conn): + for query in queries: + await conn.execute(sa.text(query)) + + await set_current_patch_level(conn, patch_lvl) + + did_patch = True + + if did_patch: + await _vacuum(conn) + + +async def _vacuum(conn: Connection, /) -> None: """Vacuum the database. This function cannot be run on a connection with an open transaction. @@ -154,13 +194,11 @@ async def set_import_progress(conn: Connection, /, progress: float) -> Progress: return current -def _connection_uri() -> str: - return f"sqlite+aiosqlite:///{config.storage_path}" - - def _new_engine() -> AsyncEngine: + uri = f"sqlite+aiosqlite:///{config.storage_path}" + return create_async_engine( - _connection_uri(), + uri, isolation_level="SERIALIZABLE", ) @@ -219,13 +257,6 @@ async def new_connection() -> AsyncGenerator[Connection, None]: async def transacted( conn: Connection, /, *, force_rollback: bool = False ) -> AsyncGenerator[None, None]: - """Start a transaction for the given connection. - - If `force_rollback` is `True` any changes will be rolled back at the end of the - transaction, unless they are explicitly committed. - Nesting transactions is allowed, but mixing values for `force_rollback` will likely - yield unexpected results. - """ transaction = contextlib.nullcontext() if conn.in_transaction() else conn.begin() async with transaction: @@ -237,17 +268,16 @@ async def transacted( await conn.rollback() -async def add(conn: Connection, /, *items: Model) -> None: - for item in items: - # Support late initializing - used for optimization. - if getattr(item, "_is_lazy", False): - assert hasattr(item, "_lazy_init") - item._lazy_init() # pyright: ignore[reportAttributeAccessIssue] +async def add(conn: Connection, /, item: Model) -> None: + # Support late initializing - used for optimization. + if getattr(item, "_is_lazy", False): + assert hasattr(item, "_lazy_init") + item._lazy_init() # pyright: ignore [reportGeneralTypeIssues] - table: sa.Table = item.__table__ - values = asplain(item, serialize=True) - stmt = table.insert().values(values) - await conn.execute(stmt) + table: sa.Table = item.__table__ + values = asplain(item, serialize=True) + stmt = table.insert().values(values) + await conn.execute(stmt) async def fetch_all( @@ -264,14 +294,17 @@ async def fetch_one( return result.first() -async def get[T: Model]( +ModelType = TypeVar("ModelType", bound=Model) + + +async def get( conn: Connection, /, - model: Type[T], + model: Type[ModelType], *, order_by: tuple[sa.Column, Literal["asc", "desc"]] | None = None, **field_values, -) -> T | None: +) -> ModelType | None: """Load a model instance from the database. Passing `field_values` allows to filter the item to load. You have to encode the @@ -294,9 +327,9 @@ async def get[T: Model]( return fromplain(model, row._mapping, serialized=True) if row else None -async def get_many[T: Model]( - conn: Connection, /, model: Type[T], **field_sets: set | list -) -> Iterable[T]: +async def get_many( + conn: Connection, /, model: Type[ModelType], **field_sets: set | list +) -> Iterable[ModelType]: """Return the items with any values matching all given field sets. This is similar to `get_all`, but instead of a scalar value a list of values @@ -313,9 +346,9 @@ async def get_many[T: Model]( return (fromplain(model, row._mapping, serialized=True) for row in rows) -async def get_all[T: Model]( - conn: Connection, /, model: Type[T], **field_values -) -> Iterable[T]: +async def get_all( + conn: Connection, /, model: Type[ModelType], **field_values +) -> Iterable[ModelType]: """Filter all items by comparing all given field values. If no filters are given, all items will be returned. @@ -332,7 +365,7 @@ async def update(conn: Connection, /, item: Model) -> None: # Support late initializing - used for optimization. if getattr(item, "_is_lazy", False): assert hasattr(item, "_lazy_init") - item._lazy_init() # pyright: ignore[reportAttributeAccessIssue] + item._lazy_init() # pyright: ignore [reportGeneralTypeIssues] table: sa.Table = item.__table__ values = asplain(item, serialize=True) @@ -433,33 +466,6 @@ async def add_or_update_rating(conn: Connection, /, rating: Rating) -> bool: return False -async def get_awards( - conn: Connection, /, imdb_ids: list[ImdbMovieId] -) -> dict[ImdbMovieId, list[Award]]: - query = ( - sa.select(Award, movies.c.imdb_id) - .join(movies, awards.c.movie_id == movies.c.id) - .where(movies.c.imdb_id.in_(imdb_ids)) - ) - rows = await fetch_all(conn, query) - awards_dict: dict[ImdbMovieId, list[Award]] = {} - for row in rows: - awards_dict.setdefault(row.imdb_id, []).append( - fromplain(Award, row._mapping, serialized=True) - ) - return awards_dict - - -async def get_movie_ids( - conn: Connection, imdb_ids: list[ImdbMovieId] -) -> dict[ImdbMovieId, MovieId]: - query = sa.select(movies.c.imdb_id, movies.c.id).where( - movies.c.imdb_id.in_(imdb_ids) - ) - rows = await fetch_all(conn, query) - return {row.imdb_id: MovieId(ULID(row.id)) for row in rows} - - def sql_escape(s: str, char: str = "#") -> str: return s.replace(char, 2 * char).replace("%", f"{char}%").replace("_", f"{char}_") @@ -475,7 +481,7 @@ async def find_ratings( include_unrated: bool = False, yearcomp: tuple[Literal["<", "=", ">"], int] | None = None, limit_rows: int = 10, - user_ids: Iterable[UserIdStr] = [], + user_ids: Iterable[str] = [], ) -> Iterable[dict[str, Any]]: conditions = [] diff --git a/unwind/imdb.py b/unwind/imdb.py index 9afdd11..cff1d68 100644 --- a/unwind/imdb.py +++ b/unwind/imdb.py @@ -3,8 +3,8 @@ import logging import re from collections import namedtuple from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import Any, AsyncIterable, Iterable +from datetime import datetime +from typing import AsyncIterable, NewType from urllib.parse import urljoin import bs4 @@ -12,11 +12,13 @@ import bs4 from . import db from .models import Movie, Rating, User from .request import adownload, asession, asoup_from_url, cache_path -from .types import ImdbMovieId, ImdbRating, ImdbUserId, Score100 -from .utils import json_dump log = logging.getLogger(__name__) +ImdbRating = NewType("ImdbRating", float) # Value range: [1.0, 10.0] +UnwindScore = NewType("UnwindScore", int) # Value range: [0, 100] +MovieId = NewType("MovieId", str) # Pattern: ttXXXXXXXX +UserId = NewType("UserId", str) # Pattern: urXXXXXXXX # div#ratings-container # div.lister-item.mode-detail @@ -72,7 +74,7 @@ def movie_url(imdb_id: str): return f"https://www.imdb.com/title/{imdb_id}/" -def imdb_rating_from_score(score: Score100) -> ImdbRating: +def imdb_rating_from_score(score: UnwindScore) -> ImdbRating: """Return the IMDb rating from an Unwind Movie score.""" assert 0 <= score <= 100 rating = round(score * 9 / 100 + 1, 1) @@ -80,7 +82,7 @@ def imdb_rating_from_score(score: Score100) -> ImdbRating: return ImdbRating(rating) -def score_from_imdb_rating(rating: ImdbRating | int) -> Score100: +def score_from_imdb_rating(rating: ImdbRating | int) -> UnwindScore: """Return the Unwind Movie score for an IMDb rating.""" # Scale IMDb's 10 point rating to our score of [0, 100]. # There's a pitfall here! @@ -89,7 +91,7 @@ def score_from_imdb_rating(rating: ImdbRating | int) -> Score100: assert 1.0 <= rating <= 10.0 score = round(100 * (rating - 1) / 9) assert 0 <= score <= 100 - return Score100(score) + return UnwindScore(score) # find_name: e.g. "Your Mom's Ratings" @@ -106,9 +108,7 @@ find_year = re.compile( r"(\([IVX]+\) )?\((?P\d{4})(–( |\d{4})| (?P[^)]+))?\)" # noqa: RUF001 ).fullmatch # find_year_2: e.g. "2024", "1971–2003", "2024–" # noqa: RUF003 -find_year_2 = re.compile( - r"(?P\d{4})(–(?P\d{4})?)?" # noqa: RUF001 -).fullmatch +find_year_2 = re.compile(r"(?P\d{4})(–(?P\d{4})?)?").fullmatch # noqa: RUF001 find_movie_id = re.compile(r"/title/(?Ptt\d+)/").search find_movie_name = re.compile(r"\d+\. (?P.+)").fullmatch # find_vote_count: e.g. "(5.9K)", "(1K)", "(8)" @@ -116,8 +116,9 @@ find_vote_count = re.compile(r"\((?P\d+(\.\d+)?K?)\)").fullmatch def _first_string(tag: bs4.Tag) -> str | None: - for s in tag.strings: - return s + for child in tag.children: + if isinstance(child, str): + return child def _tv_episode_title(series_name: str, episode_name: str) -> str: @@ -125,7 +126,7 @@ def _tv_episode_title(series_name: str, episode_name: str) -> str: def _movie_and_rating_from_item_legacy(item: bs4.Tag) -> tuple[Movie, Rating]: - genres = ((genre := item.find("span", "genre")) and genre.string) or "" + genres = (genre := item.find("span", "genre")) and genre.string or "" movie = Movie( title=item.h3.a.string.strip(), genres={s.strip() for s in genres.split(",")}, @@ -165,9 +166,7 @@ def _movie_and_rating_from_item_legacy(item: bs4.Tag) -> tuple[Movie, Rating]: ratings_item = item.find("div", "ipl-rating-widget") if match := find_rating_date(ratings_item.find_next("p", "text-muted").string): - rating.rating_date = datetime.strptime(match["date"], "%d %b %Y").replace( - tzinfo=timezone.utc - ) + rating.rating_date = datetime.strptime(match["date"], "%d %b %Y") if match := ratings_item.find("div", "ipl-rating-star--other-user"): if rating_item := match.find("span", "ipl-rating-star__rating"): rating.score = score_from_imdb_rating(float(rating_item.string)) @@ -237,16 +236,15 @@ _ForgedRequest = namedtuple("_ForgedRequest", "url headers") class _RatingsPage: ratings: list[Rating] = field(default_factory=list) next_page_url: str | None = None - imdb_user_id: ImdbUserId | None = None + imdb_user_id: UserId | None = None imdb_user_name: str | None = None -async def _load_ratings_page(url: str, user_id: ImdbUserId) -> _RatingsPage: +async def _load_ratings_page(url: str, user_id: UserId) -> _RatingsPage: """Dispatch to handlers for different ratings page versions.""" soup = await asoup_from_url(url) - if soup.find("script", id="__NEXT_DATA__", type="application/json") is not None: - return await _load_ratings_page_202407(user_id, url, soup) + if soup.find("meta", property="imdb:pageConst") is not None: return await _load_ratings_page_2024(user_id, url, soup) elif soup.find("meta", property="pageId") is not None: @@ -255,92 +253,8 @@ async def _load_ratings_page(url: str, user_id: ImdbUserId) -> _RatingsPage: raise RuntimeError("Unknown ratings page version.") -def _get_or_None(d: dict[str, Any], keys: list[str]) -> Any | None: - for k in keys: - try: - d = d[k] - except KeyError: - return None - if d is None: - break - return d - - -def _parse_movies_from_nextdata_202407(nextdata: dict) -> Iterable[Movie]: - nextratings = nextdata["props"]["pageProps"]["mainColumnData"][ - "advancedTitleSearch" - ]["edges"] - for ratingdata in nextratings: - ratingdata = ratingdata["node"]["title"] - # endYear=ratingdata["releaseYear"]["endYear"] - imdb_rating = _get_or_None(ratingdata, ["ratingsSummary", "aggregateRating"]) - runtime_s = _get_or_None(ratingdata, ["runtime", "seconds"]) - - movie = Movie( - title=ratingdata["titleText"]["text"], - original_title=_get_or_None(ratingdata, ["originalTitleText", "text"]), - release_year=ratingdata["releaseYear"]["year"], - media_type=ratingdata["titleType"]["text"], - imdb_id=ratingdata["id"], - imdb_score=( - None if imdb_rating is None else score_from_imdb_rating(imdb_rating) - ), - imdb_votes=_get_or_None(ratingdata, ["ratingsSummary", "voteCount"]), - runtime=None if runtime_s is None else int(runtime_s / 60), - genres={ - genre["genre"]["text"] for genre in ratingdata["titleGenres"]["genres"] - }, - ) - - if movie.media_type == "TV Episode": - seriesdata = ratingdata["series"]["series"] - series_original_title = seriesdata["originalTitleText"]["text"] - series_title = seriesdata["titleText"]["text"] - # series_id = seriesdata["releaseYear"]["id"] - # series_year = seriesdata["releaseYear"]["year"] - # series_endyear = seriesdata["releaseYear"]["endYear"] - movie.title = _tv_episode_title(series_title, movie.title) - movie.original_title = _tv_episode_title( - series_original_title, movie.original_title - ) - - yield movie - - -async def _load_ratings_page_202407( - user_id: ImdbUserId, url: str, soup: bs4.BeautifulSoup -) -> _RatingsPage: - """Handle the ratings page from July 2024.""" - if ( - nextjson := soup.find("script", id="__NEXT_DATA__", type="application/json") - ) is None: - raise RuntimeError("No __NEXT_DATA__ BLOB found.") - - nextdata = json.loads(nextjson.string.strip()) - userdata = nextdata["props"]["pageProps"]["aboveTheFoldData"] - - page = _RatingsPage( - imdb_user_id=userdata["authorId"], - imdb_user_name=userdata["authorName"], - ) - - movies = _parse_movies_from_nextdata_202407(nextdata) - movies_dict = {m.imdb_id: m for m in movies} - async for rating in _load_user_movie_ratings(user_id, list(movies_dict.keys())): - movie = movies_dict[rating.movie_id] - rating = Rating( - movie=movie, - score=score_from_imdb_rating(rating.imdb_rating), - rating_date=rating.rating_date, - ) - - page.ratings.append(rating) - - return page - - async def _load_ratings_page_2024( - user_id: ImdbUserId, url: str, soup: bs4.BeautifulSoup + user_id: UserId, url: str, soup: bs4.BeautifulSoup ) -> _RatingsPage: """Handle the ratings page from 2024.""" page = _RatingsPage() @@ -442,13 +356,8 @@ async def _load_ratings_page_legacy(url: str, soup: bs4.BeautifulSoup) -> _Ratin async def load_and_store_ratings( - user_id: ImdbUserId, + user_id: UserId, ) -> AsyncIterable[tuple[Rating, bool]]: - """Load user ratings from imdb.com and store them in our database. - - All loaded ratings are yielded together with the information whether each rating - was already present in our database. - """ async with db.new_connection() as conn: user = await db.get(conn, User, imdb_id=user_id) or User( imdb_id=user_id, name="", secret="" @@ -475,8 +384,7 @@ async def load_and_store_ratings( yield rating, is_updated -async def load_ratings(user_id: ImdbUserId) -> AsyncIterable[Rating]: - """Return all ratings for the given user from imdb.com.""" +async def load_ratings(user_id: UserId) -> AsyncIterable[Rating]: next_url = user_ratings_url(user_id) while next_url: @@ -486,70 +394,46 @@ async def load_ratings(user_id: ImdbUserId) -> AsyncIterable[Rating]: yield rating -def _ids_from_list_html(soup: bs4.BeautifulSoup) -> Iterable[ImdbMovieId]: - """Return all IMDb movie IDs (`tt*`) from the given soup.""" +async def _ids_from_list_html(url: str) -> AsyncIterable[MovieId]: + """Return all IMDb movie IDs (`tt*`) from the given URL.""" # document.querySelectorAll('li.ipc-metadata-list-summary-item a.ipc-title-link-wrapper') # .href: '/title/tt1213644/?ref_=chtbtm_t_1' # .text(): '1. Disaster Movie' + soup = await asoup_from_url(url) for item in soup.find_all("li", "ipc-metadata-list-summary-item"): if (link := item.find("a", "ipc-title-link-wrapper")) is not None: if (href := link.get("href")) is not None: if match_ := find_movie_id(href): - yield ImdbMovieId(match_["id"]) + yield match_["id"] -def _items_from_ldjson(soup: bs4.BeautifulSoup) -> Iterable[dict]: - """Return all items from the LD+JSON block in the given soup.""" - if (item := soup.find("script", type="application/ld+json")) is None: - raise RuntimeError("Could not find LD+JSON data.") - data = json.loads(item.string.strip()) - if data["@type"] != "ItemList": - raise RuntimeError(f"Expected ItemList, got {data['@type']!a}.") - for item in data["itemListElement"]: - yield item["item"] - - -def _ids_from_ldjson(soup: bs4.BeautifulSoup) -> Iterable[ImdbMovieId]: - for item in _items_from_ldjson(soup): - if match_ := find_movie_id(item["url"]): - yield ImdbMovieId(match_["id"]) - - -async def load_most_popular_100() -> list[ImdbMovieId]: +async def load_most_popular_100() -> list[MovieId]: """Return the IMDb's top 100 most popular movies. IMDb Charts: Most Popular Movies As determined by IMDb users """ url = "https://www.imdb.com/chart/moviemeter/" - soup = await asoup_from_url(url) - try: - ids = list(_ids_from_ldjson(soup)) - except RuntimeError: - ids = list(_ids_from_list_html(soup)) + ids = [tid async for tid in _ids_from_list_html(url)] if len(ids) != 100: raise RuntimeError(f"Expected exactly 100 items, got {len(ids)}") return ids -async def load_bottom_100() -> list[ImdbMovieId]: +async def load_bottom_100() -> list[MovieId]: """Return the IMDb's bottom 100 lowest rated movies. IMDb Charts: Lowest Rated Movies Bottom 100 as voted by IMDb users """ url = "https://www.imdb.com/chart/bottom/" - soup = await asoup_from_url(url) - try: - ids = list(_ids_from_ldjson(soup)) - except RuntimeError: - ids = list(_ids_from_list_html(soup)) + ids = [tid async for tid in _ids_from_list_html(url)] if len(ids) != 100: raise RuntimeError(f"Expected exactly 100 items, got {len(ids)}") return ids -async def load_top_250() -> list[ImdbMovieId]: +async def load_top_250() -> list[MovieId]: """Return the IMDb's top 250 highest rated movies. IMDb Charts: IMDb Top 250 Movies @@ -559,15 +443,13 @@ async def load_top_250() -> list[ImdbMovieId]: qgl_api_url = "https://caching.graphql.imdb.com/" query = { "operationName": "Top250MoviesPagination", - "variables": json_dump({"first": 250, "locale": "en-US"}), - "extensions": json_dump( - { - "persistedQuery": { - "sha256Hash": "26114ee01d97e04f65d6c8c7212ae8b7888fa57ceed105450d1fce09df749b2d", - "version": 1, - } + "variables": {"first": 250, "locale": "en-US"}, + "extensions": { + "persistedQuery": { + "sha256Hash": "26114ee01d97e04f65d6c8c7212ae8b7888fa57ceed105450d1fce09df749b2d", + "version": 1, } - ), + }, } headers = { "accept": "application/graphql+json, application/json", @@ -594,13 +476,13 @@ async def load_top_250() -> list[ImdbMovieId]: @dataclass class _UserMovieRating: - movie_id: ImdbMovieId + movie_id: MovieId rating_date: datetime imdb_rating: ImdbRating async def _load_user_movie_ratings( - user_id: ImdbUserId, movie_ids: list[ImdbMovieId] + user_id: UserId, movie_ids: list[MovieId] ) -> AsyncIterable[_UserMovieRating]: qgl_api_url = "https://api.graphql.imdb.com/" headers = { @@ -645,55 +527,3 @@ async def _load_user_movie_ratings( except KeyError as err: log.error("Unexpected data structure.", exc_info=err) raise - - -async def _load_user_ratings_202407( - user_id: ImdbUserId, movie_ids: list[ImdbMovieId] -) -> AsyncIterable[_UserMovieRating]: - """ - - This is a new API that showed up in July 2024. - It's used on a user's ratings page to load their ratings. - """ - raise NotImplementedError() - - qgl_api_url = "https://api.graphql.imdb.com/" - headers = { - "accept": "application/graphql+json, application/json", - "content-type": "application/json", - "origin": "https://www.imdb.com", - } - query = { - "operationName": "RatingsPage", - "variables": json.dumps( - { - # "after": ..., - "filter": { - "explicitContentConstraint": { - "explicitContentFilter": "INCLUDE_ADULT" - }, - "singleUserRatingConstraint": { - "filterType": "INCLUDE", - "userId": user_id, - }, - }, - "first": 100, - "locale": "en-US", - "sort": {"sortBy": "SINGLE_USER_RATING_DATE", "sortOrder": "ASC"}, - } - ), - "extensions": json.dumps( - { - "persistedQuery": { - "sha256Hash": "ae30a55f169252b5f0208d686f41aaff231d7f70bb75c257732c80234d71dbe9", - "version": 1, - } - } - ), - } - async with asession() as s: - r = await s.get(qgl_api_url, headers=headers, query=query, timeout=10) - r.raise_for_status() - data = r.json() - - # ... diff --git a/unwind/imdb_import.py b/unwind/imdb_import.py index 28792e2..5464df0 100644 --- a/unwind/imdb_import.py +++ b/unwind/imdb_import.py @@ -5,7 +5,7 @@ import logging from dataclasses import dataclass, fields from datetime import datetime, timezone from pathlib import Path -from typing import Generator, Literal, Type, overload +from typing import Generator, Literal, Type, TypeVar, overload from . import config, db, request from .db import add_or_update_many_movies @@ -14,6 +14,8 @@ from .models import Movie log = logging.getLogger(__name__) +T = TypeVar("T") + # See # - https://developer.imdb.com/non-commercial-datasets/ # - https://datasets.imdbws.com/ @@ -125,7 +127,7 @@ def read_imdb_tsv( @overload -def read_imdb_tsv[T]( +def read_imdb_tsv( path: Path, row_type: Type[T], *, unpack: Literal[True] = True ) -> Generator[T, None, None]: ... diff --git a/unwind/models.py b/unwind/models.py index d5d149f..6671a01 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -2,6 +2,7 @@ import json from dataclasses import dataclass, field from dataclasses import fields as _fields from datetime import datetime, timezone +from functools import partial from types import UnionType from typing import ( Annotated, @@ -10,33 +11,24 @@ from typing import ( Container, Literal, Mapping, - NewType, Protocol, Type, - TypeAliasType, TypedDict, + TypeVar, Union, get_args, get_origin, ) -from sqlalchemy import Column, ForeignKey, Index, Integer, String, Table +from sqlalchemy import Column, ForeignKey, Integer, String, Table from sqlalchemy.orm import registry -from .types import ( - ULID, - AwardId, - GroupId, - ImdbMovieId, - JSONObject, - JSONScalar, - MovieId, - RatingId, - Score100, - UserId, - UserIdStr, -) -from .utils import json_dump +from .types import ULID + +JSON = int | float | str | None | list["JSON"] | dict[str, "JSON"] +JSONObject = dict[str, JSON] + +T = TypeVar("T") class Model(Protocol): @@ -46,22 +38,8 @@ class Model(Protocol): mapper_registry = registry() metadata = mapper_registry.metadata -# An explicit naming convention helps Alembic do its job, -# see https://alembic.sqlalchemy.org/en/latest/naming.html. -metadata.naming_convention = { - "ix": "ix_%(column_0_label)s", - "uq": "uq_%(table_name)s_%(column_0_name)s", - "ck": "ck_%(table_name)s_%(constraint_name)s", - "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", - "pk": "pk_%(table_name)s", -} - def annotations(tp: Type) -> tuple | None: - # Support type aliases and generic aliases. - if isinstance(tp, TypeAliasType) or hasattr(tp, "__value__"): - tp = tp.__value__ - return tp.__metadata__ if hasattr(tp, "__metadata__") else None # type: ignore @@ -119,24 +97,13 @@ def optional_fields(o): yield f -def _id[T](x: T) -> T: - """Return the given argument, aka. the identity function.""" +json_dump = partial(json.dumps, separators=(",", ":")) + + +def _id(x: T) -> T: return x -def _unpack(type_: Any) -> Any: - """Return the wrapped type.""" - # Handle type aliases. - if isinstance(type_, TypeAliasType): - return _unpack(type_.__value__) - - # Handle newtypes. - if isinstance(type_, NewType): - return _unpack(type_.__supertype__) - - return type_ - - def asplain( o: object, *, filter_fields: Container[str] | None = None, serialize: bool = False ) -> dict[str, Any]: @@ -158,16 +125,13 @@ def asplain( if filter_fields is not None and f.name not in filter_fields: continue - target: Any = _unpack(f.type) - + target: Any = f.type # XXX this doesn't properly support any kind of nested types if (otype := optional_type(f.type)) is not None: target = otype if (otype := get_origin(target)) is not None: target = otype - target = _unpack(target) - v = getattr(o, f.name) if is_optional(f.type) and v is None: d[f.name] = None @@ -184,33 +148,28 @@ def asplain( assert isinstance(v, list) d[f.name] = dump(list(v)) elif target in {bool, str, int, float}: - assert isinstance(v, target), ( - f"Type mismatch: {f.name!a} ({target!a} != {type(v)!a})" - ) - d[f.name] = v - elif target in {Literal}: - assert isinstance(v, JSONScalar.__value__) + assert isinstance( + v, target + ), f"Type mismatch: {f.name} ({target} != {type(v)})" d[f.name] = v else: - raise ValueError(f"Unsupported value type: {f.name!a}: {type(v)!a}") + raise ValueError(f"Unsupported value type: {f.name}: {type(v)}") return d -def fromplain[T](cls: Type[T], d: Mapping, *, serialized: bool = False) -> T: +def fromplain(cls: Type[T], d: Mapping, *, serialized: bool = False) -> T: """Return an instance of the given model using the given data. If `serialized` is `True`, collection types (lists, dicts, etc.) will be deserialized from string. This is the opposite operation of `serialize` for `asplain`. - Fields in the data that cannot be mapped to the given type are simply ignored. """ load = json.loads if serialized else _id dd: JSONObject = {} for f in fields(cls): - target: Any = _unpack(f.type) - + target: Any = f.type otype = optional_type(f.type) is_opt = otype is not None if is_opt: @@ -218,17 +177,9 @@ def fromplain[T](cls: Type[T], d: Mapping, *, serialized: bool = False) -> T: if (xtype := get_origin(target)) is not None: target = xtype - target = _unpack(target) - v = d[f.name] if is_opt and v is None: dd[f.name] = v - elif target is Literal: - # Support literal types. - vals = get_args(f.type.__value__) - if v not in vals: - raise ValueError(f"Invalid value: {f.name!a}: {v!a}") - dd[f.name] = v elif isinstance(v, target): dd[f.name] = v elif target in {set, list}: @@ -245,38 +196,27 @@ def fromplain[T](cls: Type[T], d: Mapping, *, serialized: bool = False) -> T: def validate(o: object) -> None: for f in fields(o): - ftype = _unpack(f.type) - - v = getattr(o, f.name) - vtype = type(v) - if vtype is ftype: + vtype = type(getattr(o, f.name)) + if vtype is f.type: continue - origin = get_origin(ftype) + origin = get_origin(f.type) if origin is vtype: continue - is_union = isinstance(ftype, UnionType) or origin is Union + is_union = isinstance(f.type, UnionType) or origin is Union if is_union: # Support unioned types. - utypes = get_args(ftype) - utypes = [_unpack(t) for t in utypes] + utypes = get_args(f.type) if vtype in utypes: continue # Support generic types (set[str], list[int], etc.) - gtypes = [_unpack(g) for u in utypes if (g := get_origin(u)) is not None] + gtypes = [g for u in utypes if (g := get_origin(u)) is not None] if any(vtype is gtype for gtype in gtypes): continue - if origin is Literal: - # Support literal types. - vals = get_args(ftype) - if v in vals: - continue - raise ValueError(f"Invalid value: {f.name!a}: {v!a}") - - raise ValueError(f"Invalid value type: {f.name!a}: {vtype!a}") + raise ValueError(f"Invalid value type: {f.name}: {vtype}") def utcnow() -> datetime: @@ -284,6 +224,23 @@ def utcnow() -> datetime: return datetime.now(timezone.utc) +@mapper_registry.mapped +@dataclass +class DbPatch: + __table__: ClassVar[Table] = Table( + "db_patches", + metadata, + Column("id", Integer, primary_key=True), + Column("current", String), + ) + + id: int + current: str + + +db_patches = DbPatch.__table__ + + @mapper_registry.mapped @dataclass class Progress: @@ -355,15 +312,15 @@ class Movie: Column("updated", String, nullable=False), # datetime ) - id: MovieId = field(default_factory=ULID) + id: ULID = field(default_factory=ULID) title: str = None # canonical title (usually English) original_title: str | None = ( None # original title (usually transscribed to latin script) ) release_year: int = None # canonical release date media_type: str = None - imdb_id: ImdbMovieId = None - imdb_score: Score100 | None = None # range: [0,100] + imdb_id: str = None + imdb_score: int | None = None # range: [0,100] imdb_votes: int | None = None runtime: int | None = None # minutes genres: set[str] | None = None @@ -408,10 +365,10 @@ dataclass containing the ID of the linked data. The contents of the Relation are ignored or discarded when using `asplain`, `fromplain`, and `validate`. """ -type Relation[T] = Annotated[T | None, _RelationSentinel] +Relation = Annotated[T | None, _RelationSentinel] -type Access = Literal[ +Access = Literal[ "r", # read "i", # index "w", # write @@ -436,8 +393,8 @@ class User: Column("groups", String, nullable=False), # JSON array ) - id: UserId = field(default_factory=ULID) - imdb_id: ImdbMovieId = None + id: ULID = field(default_factory=ULID) + imdb_id: str = None name: str = None # canonical user name secret: str = None groups: list[UserGroup] = field(default_factory=list) @@ -456,9 +413,6 @@ class User: self.groups.append({"id": group_id, "access": access}) -users = User.__table__ - - @mapper_registry.mapped @dataclass class Rating: @@ -474,15 +428,15 @@ class Rating: Column("finished", Integer), # bool ) - id: RatingId = field(default_factory=ULID) + id: ULID = field(default_factory=ULID) - movie_id: MovieId = None + movie_id: ULID = None movie: Relation[Movie] = None - user_id: UserId = None + user_id: ULID = None user: Relation[User] = None - score: Score100 = None # range: [0,100] + score: int = None # range: [0,100] rating_date: datetime = None favorite: bool | None = None finished: bool | None = None @@ -501,11 +455,64 @@ class Rating: ratings = Rating.__table__ -Index("ratings_index", ratings.c.movie_id, ratings.c.user_id, unique=True) + + +# TODO +# - distinguish between ratings & watches +# - they are completely separate +# - I can rate something at any time, without having watched it, e.g. in a discussion with a friend I change my opinion on a movie +# - I can watch something without having fully formed an opinion yet, i.e. I don't want to rate it yet + +# How are Rating.favorite and Rating.finished linked to Watches? +# - is Rating.favorite automatically Watches[-1].favorite, or any(Watches.favorite)? +# - Rating.favorite is nullable, so unless it's explicitly set we can default to Watches +# - is Rating.finished automatically any(Watches.finished)? +# - Rating.finished is nullable, so unless it's explicitly set we can default to Watches +# - can Rating.finished be set without a Watch? +# - yes +# - can Rating.favorite be set without a Watch? +# - yes + + +@mapper_registry.mapped +@dataclass +class Watch: + """A "check-in" event, the user started watching a movie.""" + + __table__: ClassVar[Table] = Table( + "watches", + metadata, + Column("id", String, primary_key=True), # ULID + Column("movie_id", ForeignKey("movies.id"), nullable=False), # ULID + Column("user_id", ForeignKey("users.id"), nullable=False), # ULID + Column("started", String, nullable=False), # datetime + Column("finished", String), # datetime + Column("geoloc", String), # geo coords + Column("score", Integer), # + Column("favorite", Integer), # bool + ) + + id: ULID = field(default_factory=ULID) + + movie_id: ULID = None + movie: Relation[Movie] = None + + user_id: ULID = None + user: Relation[User] = None + + started: datetime | None = None + finished: datetime | None = None + + geoloc: str | None = None + score: int | None = None # range: [0,100] + favorite: bool | None = None + + +watches = Rating.__table__ class GroupUser(TypedDict): - id: UserIdStr + id: str name: str @@ -520,72 +527,6 @@ class Group: Column("users", String, nullable=False), # JSON array ) - id: GroupId = field(default_factory=ULID) + id: ULID = field(default_factory=ULID) name: str = None users: list[GroupUser] = field(default_factory=list) - - -type AwardCategory = Literal[ - "imdb-top-250", "imdb-bottom-100", "imdb-pop-100", "oscars" -] - - -@mapper_registry.mapped -@dataclass -class Award: - __table__: ClassVar[Table] = Table( - "awards", - metadata, - Column("id", String, primary_key=True), # ULID - Column("movie_id", ForeignKey("movies.id"), nullable=False), # ULID - Column( - "category", String, nullable=False - ), # Enum: "imdb-top-250", "imdb-bottom-100", "imdb-pop-100", "oscars", ... - Column( - "details", String, nullable=False - ), # e.g. "23" (position in list), "2024, nominee, best director", "1977, winner, best picture", ... - Column("created", String, nullable=False), # datetime - Column("updated", String, nullable=False), # datetime - ) - - id: AwardId = field(default_factory=ULID) - - movie_id: MovieId = None - movie: Relation[Movie] = None - - category: AwardCategory = None - details: str = None - - created: datetime = field(default_factory=utcnow) - updated: datetime = field(default_factory=utcnow) - - @property - def _details(self) -> JSONObject: - return json.loads(self.details or "{}") - - @_details.setter - def _details(self, details: JSONObject): - self.details = json_dump(details) - - @property - def position(self) -> int: - return self._details["position"] - - @position.setter - def position(self, position: int): - details = self._details - details["position"] = position - self._details = details - - @property - def name(self) -> str: - return self._details["name"] - - @name.setter - def name(self, name: str): - details = self._details - details["name"] = name - self._details = details - - -awards = Award.__table__ diff --git a/unwind/request.py b/unwind/request.py index 46d1e9b..f12936b 100644 --- a/unwind/request.py +++ b/unwind/request.py @@ -11,7 +11,7 @@ from hashlib import md5 from pathlib import Path from random import random from time import sleep, time -from typing import Any, Callable, cast, overload +from typing import Any, Callable, ParamSpec, TypeVar, cast, overload import bs4 import httpx @@ -24,10 +24,13 @@ if config.debug and config.cachedir: config.cachedir.mkdir(exist_ok=True) -_ASession_T = httpx.AsyncClient -type _Response_T = httpx.Response +_shared_asession = None -_shared_asession: _ASession_T | None = None +_ASession_T = httpx.AsyncClient +_Response_T = httpx.Response + +_T = TypeVar("_T") +_P = ParamSpec("_P") @asynccontextmanager @@ -56,17 +59,17 @@ async def asession(): _shared_asession = None -def _throttle[T, **P]( +def _throttle( times: int, per_seconds: float, jitter: Callable[[], float] | None = None -) -> Callable[[Callable[P, T]], Callable[P, T]]: +) -> Callable[[Callable[_P, _T]], Callable[_P, _T]]: calls: deque[float] = deque(maxlen=times) if jitter is None: jitter = lambda: 0.0 # noqa: E731 - def decorator(func: Callable[P, T]) -> Callable[P, T]: + def decorator(func: Callable[_P, _T]) -> Callable[_P, _T]: @wraps(func) - def inner(*args: P.args, **kwds: P.kwargs): + def inner(*args: _P.args, **kwds: _P.kwargs): # clean up while calls: if calls[0] + per_seconds > time(): diff --git a/unwind/sql/00000000-init-0.sql b/unwind/sql/00000000-init-0.sql new file mode 100644 index 0000000..d0bd446 --- /dev/null +++ b/unwind/sql/00000000-init-0.sql @@ -0,0 +1,36 @@ +PRAGMA foreign_keys = ON;; + +CREATE TABLE IF NOT EXISTS users ( + id TEXT NOT NULL PRIMARY KEY, + imdb_id TEXT NOT NULL UNIQUE, + name TEXT NOT NULL +);; + +CREATE TABLE IF NOT EXISTS movies ( + id TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + release_year NUMBER NOT NULL, + media_type TEXT NOT NULL, + imdb_id TEXT NOT NULL UNIQUE, + score NUMBER NOT NULL, + runtime NUMBER, + genres TEXT NOT NULL, + updated TEXT NOT NULL +);; + +CREATE TABLE IF NOT EXISTS ratings ( + id TEXT NOT NULL PRIMARY KEY, + movie_id TEXT NOT NULL, + user_id TEXT NOT NULL, + score NUMBER NOT NULL, + rating_date TEXT NOT NULL, + favorite NUMBER, + finished NUMBER, + FOREIGN KEY(movie_id) REFERENCES movies(id), + FOREIGN KEY(user_id) REFERENCES users(id) +);; + +CREATE UNIQUE INDEX IF NOT EXISTS ratings_index ON ratings ( + movie_id, + user_id +);; diff --git a/unwind/sql/00000000-init-1.sql b/unwind/sql/00000000-init-1.sql new file mode 100644 index 0000000..85d40a6 --- /dev/null +++ b/unwind/sql/00000000-init-1.sql @@ -0,0 +1,40 @@ +-- add original_title to movies table + +-- see https://www.sqlite.org/lang_altertable.html#caution +-- 1. Create new table +-- 2. Copy data +-- 3. Drop old table +-- 4. Rename new into old + +CREATE TABLE _migrate_movies ( + id TEXT NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + original_title TEXT, + release_year NUMBER NOT NULL, + media_type TEXT NOT NULL, + imdb_id TEXT NOT NULL UNIQUE, + score NUMBER, + runtime NUMBER, + genres TEXT NOT NULL, + updated TEXT NOT NULL +);; + +INSERT INTO _migrate_movies +SELECT + id, + title, + NULL, + release_year, + media_type, + imdb_id, + score, + runtime, + genres, + updated +FROM movies +WHERE true;; + +DROP TABLE movies;; + +ALTER TABLE _migrate_movies +RENAME TO movies;; diff --git a/unwind/sql/00000000-init-2.sql b/unwind/sql/00000000-init-2.sql new file mode 100644 index 0000000..68fad70 --- /dev/null +++ b/unwind/sql/00000000-init-2.sql @@ -0,0 +1,46 @@ +-- only set original_title if it differs from title, +-- and normalize media_type with an extra table. + +CREATE TABLE mediatypes ( + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL UNIQUE +);; + +INSERT INTO mediatypes (name) +SELECT DISTINCT media_type +FROM movies +WHERE true;; + +CREATE TABLE _migrate_movies ( + id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + original_title TEXT, + release_year INTEGER NOT NULL, + media_type_id INTEGER NOT NULL, + imdb_id TEXT NOT NULL UNIQUE, + score INTEGER, + runtime INTEGER, + genres TEXT NOT NULL, + updated TEXT NOT NULL, + FOREIGN KEY(media_type_id) REFERENCES mediatypes(id) +);; + +INSERT INTO _migrate_movies +SELECT + id, + title, + (CASE WHEN original_title=title THEN NULL ELSE original_title END), + release_year, + (SELECT id FROM mediatypes WHERE name=media_type) AS media_type_id, + imdb_id, + score, + runtime, + genres, + updated +FROM movies +WHERE true;; + +DROP TABLE movies;; + +ALTER TABLE _migrate_movies +RENAME TO movies;; diff --git a/unwind/sql/00000000-init-3.sql b/unwind/sql/00000000-init-3.sql new file mode 100644 index 0000000..98380c7 --- /dev/null +++ b/unwind/sql/00000000-init-3.sql @@ -0,0 +1,62 @@ +-- add convenient view for movies + +CREATE VIEW IF NOT EXISTS movies_view +AS SELECT + movies.id, + movies.title, + movies.original_title, + movies.release_year, + mediatypes.name AS media_type, + movies.imdb_id, + movies.score, + movies.runtime, + movies.genres, + movies.updated +FROM movies +JOIN mediatypes ON mediatypes.id=movies.media_type_id;; + +CREATE TRIGGER IF NOT EXISTS insert_movies_view + INSTEAD OF INSERT + ON movies_view +BEGIN + INSERT INTO movies ( + id, + title, + original_title, + release_year, + media_type_id, + imdb_id, + score, + runtime, + genres, + updated + ) VALUES ( + NEW.id, + NEW.title, + NEW.original_title, + NEW.release_year, + (SELECT id FROM mediatypes WHERE name=NEW.media_type), + NEW.imdb_id, + NEW.score, + NEW.runtime, + NEW.genres, + NEW.updated + ); +END;; + +CREATE TRIGGER IF NOT EXISTS update_movies_view + INSTEAD OF UPDATE OF media_type + ON movies_view +BEGIN + UPDATE movies + SET media_type_id=(SELECT id FROM mediatypes WHERE name=NEW.media_type) + WHERE id=OLD.id; +END;; + +CREATE TRIGGER IF NOT EXISTS delete_movies_view + INSTEAD OF DELETE + ON movies_view +BEGIN + DELETE FROM movies + WHERE movies.id=OLD.id; +END;; diff --git a/unwind/sql/00000000-init-4.sql b/unwind/sql/00000000-init-4.sql new file mode 100644 index 0000000..984ef37 --- /dev/null +++ b/unwind/sql/00000000-init-4.sql @@ -0,0 +1,37 @@ +-- denormalize movie media_type + +CREATE TABLE _migrate_movies ( + id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + original_title TEXT, + release_year INTEGER NOT NULL, + media_type TEXT NOT NULL, + imdb_id TEXT NOT NULL UNIQUE, + score INTEGER, + runtime INTEGER, + genres TEXT NOT NULL, + updated TEXT NOT NULL +);; + +INSERT INTO _migrate_movies +SELECT + id, + title, + original_title, + release_year, + (SELECT name FROM mediatypes WHERE id=media_type_id) AS media_type, + imdb_id, + score, + runtime, + genres, + updated +FROM movies +WHERE true;; + +DROP VIEW movies_view;; +DROP TABLE mediatypes;; + +DROP TABLE movies;; + +ALTER TABLE _migrate_movies +RENAME TO movies;; diff --git a/unwind/sql/00000001-fix-db.sql.disabled b/unwind/sql/00000001-fix-db.sql.disabled new file mode 100644 index 0000000..e6376a8 --- /dev/null +++ b/unwind/sql/00000001-fix-db.sql.disabled @@ -0,0 +1,2 @@ +-- see the commit of this file for details. +;; diff --git a/unwind/sql/20210705-224139.sql b/unwind/sql/20210705-224139.sql new file mode 100644 index 0000000..e714b4e --- /dev/null +++ b/unwind/sql/20210705-224139.sql @@ -0,0 +1,8 @@ +-- add groups table + +CREATE TABLE groups ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + secret TEXT NOT NULL, + users TEXT NOT NULL -- JSON array +);; diff --git a/unwind/sql/20210711-172808--progress-table.sql b/unwind/sql/20210711-172808--progress-table.sql new file mode 100644 index 0000000..1ee6a5f --- /dev/null +++ b/unwind/sql/20210711-172808--progress-table.sql @@ -0,0 +1,7 @@ +-- add progress table + +CREATE TABLE progress ( + id TEXT PRIMARY KEY NOT NULL, + state TEXT NOT NULL, + started TEXT NOT NULL +);; diff --git a/unwind/sql/20210720-213416.sql b/unwind/sql/20210720-213416.sql new file mode 100644 index 0000000..286e094 --- /dev/null +++ b/unwind/sql/20210720-213416.sql @@ -0,0 +1,36 @@ +-- add IMDb vote count + +CREATE TABLE _migrate_movies ( + id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + original_title TEXT, + release_year INTEGER NOT NULL, + media_type TEXT NOT NULL, + imdb_id TEXT NOT NULL UNIQUE, + imdb_score INTEGER, + imdb_votes INTEGER, + runtime INTEGER, + genres TEXT NOT NULL, + updated TEXT NOT NULL +);; + +INSERT INTO _migrate_movies +SELECT + id, + title, + original_title, + release_year, + media_type, + imdb_id, + score AS imdb_score, + NULL AS imdb_votes, + runtime, + genres, + updated +FROM movies +WHERE true;; + +DROP TABLE movies;; + +ALTER TABLE _migrate_movies +RENAME TO movies;; diff --git a/unwind/sql/20210720-223416.sql b/unwind/sql/20210720-223416.sql new file mode 100644 index 0000000..95e1b78 --- /dev/null +++ b/unwind/sql/20210720-223416.sql @@ -0,0 +1,24 @@ +-- add IMDb vote count + +CREATE TABLE _migrate_progress ( + id TEXT PRIMARY KEY NOT NULL, + type TEXT NOT NULL, + state TEXT NOT NULL, + started TEXT NOT NULL, + stopped TEXT +);; + +INSERT INTO _migrate_progress +SELECT + id, + 'import-imdb-movies' AS type, + state, + started, + NULL AS stopped +FROM progress +WHERE true;; + +DROP TABLE progress;; + +ALTER TABLE _migrate_progress +RENAME TO progress;; diff --git a/unwind/sql/20210721-213417.sql b/unwind/sql/20210721-213417.sql new file mode 100644 index 0000000..33e891a --- /dev/null +++ b/unwind/sql/20210721-213417.sql @@ -0,0 +1,38 @@ +-- add creation timestamp to movies + +CREATE TABLE _migrate_movies ( + id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + original_title TEXT, + release_year INTEGER NOT NULL, + media_type TEXT NOT NULL, + imdb_id TEXT NOT NULL UNIQUE, + imdb_score INTEGER, + imdb_votes INTEGER, + runtime INTEGER, + genres TEXT NOT NULL, + created TEXT NOT NULL, + updated TEXT NOT NULL +);; + +INSERT INTO _migrate_movies +SELECT + id, + title, + original_title, + release_year, + media_type, + imdb_id, + imdb_score, + imdb_votes, + runtime, + genres, + updated AS created, + updated +FROM movies +WHERE true;; + +DROP TABLE movies;; + +ALTER TABLE _migrate_movies +RENAME TO movies;; diff --git a/unwind/sql/20210728-223416.sql b/unwind/sql/20210728-223416.sql new file mode 100644 index 0000000..1581060 --- /dev/null +++ b/unwind/sql/20210728-223416.sql @@ -0,0 +1,24 @@ +-- add IMDb vote count + +CREATE TABLE _migrate_progress ( + id TEXT PRIMARY KEY NOT NULL, + type TEXT NOT NULL, + state TEXT NOT NULL, + started TEXT NOT NULL, + stopped TEXT +);; + +INSERT INTO _migrate_progress +SELECT + id, + type, + '{"percent":' || state || '}' AS state, + started, + stopped +FROM progress +WHERE true;; + +DROP TABLE progress;; + +ALTER TABLE _migrate_progress +RENAME TO progress;; diff --git a/unwind/sql/20210801-201151--add-user-secret.sql b/unwind/sql/20210801-201151--add-user-secret.sql new file mode 100644 index 0000000..3294a56 --- /dev/null +++ b/unwind/sql/20210801-201151--add-user-secret.sql @@ -0,0 +1,22 @@ +-- add secret to users + +CREATE TABLE _migrate_users ( + id TEXT PRIMARY KEY NOT NULL, + imdb_id TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + secret TEXT NOT NULL +);; + +INSERT INTO _migrate_users +SELECT + id, + imdb_id, + name, + '' AS secret +FROM users +WHERE true;; + +DROP TABLE users;; + +ALTER TABLE _migrate_users +RENAME TO users;; diff --git a/unwind/sql/20210802-212312--add-group-admins.sql b/unwind/sql/20210802-212312--add-group-admins.sql new file mode 100644 index 0000000..13f3105 --- /dev/null +++ b/unwind/sql/20210802-212312--add-group-admins.sql @@ -0,0 +1,45 @@ +-- add group admins + +--- remove secrets from groups +CREATE TABLE _migrate_groups ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + users TEXT NOT NULL -- JSON array +);; + +INSERT INTO _migrate_groups +SELECT + id, + name, + users +FROM groups +WHERE true;; + +DROP TABLE groups;; + +ALTER TABLE _migrate_groups +RENAME TO groups;; + +--- add group access to users +CREATE TABLE _migrate_users ( + id TEXT PRIMARY KEY NOT NULL, + imdb_id TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + secret TEXT NOT NULL, + groups TEXT NOT NULL -- JSON array +);; + +INSERT INTO _migrate_users +SELECT + id, + imdb_id, + name, + secret, + '[]' AS groups +FROM users +WHERE true;; + +DROP TABLE users;; + +ALTER TABLE _migrate_users +RENAME TO users;; diff --git a/unwind/sql/20240511-001949--remove-genres-notnull.sql b/unwind/sql/20240511-001949--remove-genres-notnull.sql new file mode 100644 index 0000000..98a7c16 --- /dev/null +++ b/unwind/sql/20240511-001949--remove-genres-notnull.sql @@ -0,0 +1,38 @@ +-- remove NOTNULL constraint from movies.genres + +CREATE TABLE _migrate_movies ( + id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + original_title TEXT, + release_year INTEGER NOT NULL, + media_type TEXT NOT NULL, + imdb_id TEXT NOT NULL UNIQUE, + imdb_score INTEGER, + imdb_votes INTEGER, + runtime INTEGER, + genres TEXT, + created TEXT NOT NULL, + updated TEXT NOT NULL +);; + +INSERT INTO _migrate_movies +SELECT + id, + title, + original_title, + release_year, + media_type, + imdb_id, + imdb_score, + imdb_votes, + runtime, + genres, + created, + updated +FROM movies +WHERE true;; + +DROP TABLE movies;; + +ALTER TABLE _migrate_movies +RENAME TO movies;; diff --git a/unwind/types.py b/unwind/types.py index e035f13..94c0e00 100644 --- a/unwind/types.py +++ b/unwind/types.py @@ -1,13 +1,9 @@ import re -from typing import NewType, cast +from typing import cast import ulid from ulid.hints import Buffer -type JSONScalar = int | float | str | None -type JSON = JSONScalar | list["JSON"] | dict[str, "JSON"] -type JSONObject = dict[str, JSON] - class ULID(ulid.ULID): """Extended ULID type. @@ -33,16 +29,3 @@ class ULID(ulid.ULID): buffer = cast(memoryview, ulid.new().memory) super().__init__(buffer) - - -AwardId = NewType("AwardId", ULID) -GroupId = NewType("GroupId", ULID) -ImdbMovieId = NewType("ImdbMovieId", str) # Pattern: ttXXXXXXXX -ImdbRating = NewType("ImdbRating", float) # Value range: [1.0, 10.0] -ImdbUserId = NewType("ImdbUserId", str) # Pattern: urXXXXXXXX -MovieId = NewType("MovieId", ULID) -MovieIdStr = NewType("MovieIdStr", str) -RatingId = NewType("RatingId", ULID) -Score100 = NewType("Score100", int) # Value range: [0, 100] -UserId = NewType("UserId", ULID) -UserIdStr = NewType("UserIdStr", str) diff --git a/unwind/utils.py b/unwind/utils.py index d1733a8..6ad4d32 100644 --- a/unwind/utils.py +++ b/unwind/utils.py @@ -1,12 +1,8 @@ import base64 import hashlib -import json import secrets -from functools import partial from typing import Any, TypedDict -json_dump = partial(json.dumps, separators=(",", ":")) - def b64encode(b: bytes) -> str: return base64.b64encode(b).decode().rstrip("=") diff --git a/unwind/web.py b/unwind/web.py index 3812e79..5cfeda9 100644 --- a/unwind/web.py +++ b/unwind/web.py @@ -3,7 +3,7 @@ import contextlib import logging import secrets from json.decoder import JSONDecodeError -from typing import Any, Literal, Never, TypeGuard, overload +from typing import Literal, overload from starlette.applications import Starlette from starlette.authentication import ( @@ -20,15 +20,15 @@ from starlette.middleware import Middleware from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.cors import CORSMiddleware from starlette.middleware.gzip import GZipMiddleware -from starlette.requests import HTTPConnection, Request +from starlette.requests import HTTPConnection from starlette.responses import JSONResponse from starlette.routing import Mount, Route from . import config, db, imdb, imdb_import, web_models from .db import close_connection_pool, find_movies, find_ratings, open_connection_pool from .middleware.responsetime import ResponseTimeMiddleware -from .models import Access, Group, Movie, User, asplain -from .types import JSON, ULID +from .models import Group, Movie, User, asplain +from .types import ULID from .utils import b64decode, b64encode, phc_compare, phc_scrypt log = logging.getLogger(__name__) @@ -49,12 +49,12 @@ class BearerAuthBackend(AuthenticationBackend): self.admin_tokens = {v: k for k, v in credentials.items()} async def authenticate(self, conn: HTTPConnection): - if "Authorization" not in conn.headers: + if "authorization" not in conn.headers: return # XXX should we remove the auth header after reading, for security reasons? - auth = conn.headers["Authorization"] + auth = conn.headers["authorization"] try: scheme, credentials = auth.split() except ValueError as err: @@ -62,32 +62,33 @@ class BearerAuthBackend(AuthenticationBackend): roles = [] - if scheme.lower() == "bearer": - is_admin = credentials in self.admin_tokens - if not is_admin: + match scheme.lower(): + case "bearer": + is_admin = credentials in self.admin_tokens + if not is_admin: + return + name = self.admin_tokens[credentials] + user = SimpleUser(name) + roles.append("admin") + + case "basic": + try: + name, secret = b64decode(credentials).decode().split(":") + except Exception as err: + raise AuthenticationError("Invalid auth credentials") from err + user = AuthedUser(name, secret) + + case _: return - name = self.admin_tokens[credentials] - user = SimpleUser(name) - roles.append("admin") - - elif scheme.lower() == "basic": - try: - name, secret = b64decode(credentials).decode().split(":") - except Exception as err: - raise AuthenticationError("Invalid auth credentials") from err - user = AuthedUser(name, secret) - - else: - return return AuthCredentials(["authenticated", *roles]), user -def truthy(s: str | None) -> bool: +def truthy(s: str): return bool(s) and s.lower() in {"1", "yes", "true"} -type _Yearcomp = Literal["<", "=", ">"] +_Yearcomp = Literal["<", "=", ">"] def yearcomp(s: str) -> tuple[_Yearcomp, int] | None: @@ -103,7 +104,7 @@ def yearcomp(s: str) -> tuple[_Yearcomp, int] | None: def as_int( - x: Any, *, max: int | None = None, min: int | None = 1, default: int | None = None + x, *, max: int | None = None, min: int | None = 1, default: int | None = None ) -> int: try: if not isinstance(x, int): @@ -121,9 +122,9 @@ def as_int( return default -def as_ulid(s: Any) -> ULID: +def as_ulid(s: str) -> ULID: try: - if not isinstance(s, str) or not s: + if not s: raise ValueError("Invalid ULID.") return ULID(s) @@ -133,17 +134,14 @@ def as_ulid(s: Any) -> ULID: @overload -async def json_from_body(request: Request) -> dict[str, JSON]: ... +async def json_from_body(request) -> dict: ... @overload -async def json_from_body(request: Request, keys: list[str]) -> list[JSON]: ... +async def json_from_body(request, keys: list[str]) -> list: ... -async def json_from_body( - request: Request, keys: list[str] | None = None -) -> dict[str, JSON] | list[JSON]: - data: dict[str, JSON] +async def json_from_body(request, keys: list[str] | None = None) -> dict | list: if not await request.body(): data = {} @@ -154,7 +152,7 @@ async def json_from_body( raise HTTPException(422, "Invalid JSON content.") from err if not isinstance(data, dict): - raise HTTPException(422, f"Invalid JSON type: {type(data)!a}") + raise HTTPException(422, f"Unexpected JSON root type: {type(data)!a}.") if not keys: return data @@ -162,14 +160,14 @@ async def json_from_body( try: return [data[k] for k in keys] except KeyError as err: - raise HTTPException(422, f"Missing data for key: {err.args[0]}") from err + raise HTTPException(422, f"Missing data for key: {err.args[0]!a}") from err -def is_admin(request: Request) -> bool: +def is_admin(request): return "admin" in request.auth.scopes -async def auth_user(request: Request) -> User | None: +async def auth_user(request) -> User | None: if not isinstance(request.user, AuthedUser): return @@ -198,7 +196,7 @@ def route(path: str, *, methods: list[str] | None = None, **kwds): @route("/groups/{group_id}/ratings") -async def get_ratings_for_group(request: Request) -> JSONResponse: +async def get_ratings_for_group(request): group_id = as_ulid(request.path_params["group_id"]) async with db.new_connection() as conn: @@ -235,14 +233,11 @@ async def get_ratings_for_group(request: Request) -> JSONResponse: user_ids=user_ids, ) - ratings = [web_models.Rating(**r) for r in rows] + ratings = (web_models.Rating(**r) for r in rows) - async with db.new_connection() as conn: - awards = await db.get_awards(conn, imdb_ids=[r.movie_imdb_id for r in ratings]) + aggr = web_models.aggregate_ratings(ratings, user_ids) - aggrs = web_models.aggregate_ratings(ratings, user_ids, awards_dict=awards) - - resp = tuple(asplain(r) for r in aggrs) + resp = tuple(asplain(r) for r in aggr) return JSONResponse(resp) @@ -259,13 +254,13 @@ def not_found(reason: str = "Not Found"): return JSONResponse({"error": reason}, status_code=404) -def not_implemented() -> Never: +def not_implemented(): raise HTTPException(404, "Not yet implemented.") @route("/movies") @requires(["authenticated"]) -async def list_movies(request: Request) -> JSONResponse: +async def list_movies(request): params = request.query_params user = await auth_user(request) @@ -280,7 +275,7 @@ async def list_movies(request: Request) -> JSONResponse: if not group: return not_found("Group not found.") - is_allowed = is_admin(request) or (user and user.has_access(group_id)) + is_allowed = is_admin(request) or user and user.has_access(group_id) if not is_allowed: return forbidden("No access to group.") @@ -290,7 +285,7 @@ async def list_movies(request: Request) -> JSONResponse: user_id = as_ulid(user_id) # Currently a user may only directly access their own ratings. - is_allowed = is_admin(request) or (user and user.id == user_id) + is_allowed = is_admin(request) or user and user.id == user_id if not is_allowed: return forbidden("No access to user.") @@ -338,13 +333,13 @@ async def list_movies(request: Request) -> JSONResponse: @route("/movies", methods=["POST"]) @requires(["authenticated", "admin"]) -async def add_movie(request: Request) -> JSONResponse: +async def add_movie(request): not_implemented() @route("/movies/_reload_imdb", methods=["GET"]) @requires(["authenticated", "admin"]) -async def progress_for_load_imdb_movies(request: Request) -> JSONResponse: +async def progress_for_load_imdb_movies(request): async with db.new_connection() as conn: progress = await db.get_import_progress(conn) if not progress: @@ -375,12 +370,40 @@ async def progress_for_load_imdb_movies(request: Request) -> JSONResponse: return JSONResponse(resp) +@route("/users/{user_id}/[movies/{movie_id}/]watches", methods=["POST"]) +@requires(["authenticated"]) +async def add_watch_to_user(request): + # { + # id + # movie_id + # location (gps) + # started + # finished + # score + # fav + # } + user_id = as_ulid(request.path_params["user_id"]) + + geoloc, started = await json_from_body(request, ["geoloc", "started"]) + + +@route("/users/{user_id}/[movies/{movie_id}/]watches/{watch_id}", methods=["PUT"]) +@requires(["authenticated"]) +async def update_watch_for_user(request): + user_id = as_ulid(request.path_params["user_id"]) + watch_id = as_ulid(request.path_params["watch_id"]) + + finished, score, favorite = await json_from_body( + request, ["finished", "score", "favorite"] + ) + + _import_lock = asyncio.Lock() @route("/movies/_reload_imdb", methods=["POST"]) @requires(["authenticated", "admin"]) -async def load_imdb_movies(request: Request) -> JSONResponse: +async def load_imdb_movies(request): params = request.query_params force = truthy(params.get("force")) @@ -404,7 +427,7 @@ async def load_imdb_movies(request: Request) -> JSONResponse: @route("/users") @requires(["authenticated", "admin"]) -async def list_users(request: Request) -> JSONResponse: +async def list_users(request): async with db.new_connection() as conn: users = await db.get_all(conn, User) @@ -413,7 +436,7 @@ async def list_users(request: Request) -> JSONResponse: @route("/users", methods=["POST"]) @requires(["authenticated", "admin"]) -async def add_user(request: Request) -> JSONResponse: +async def add_user(request): name, imdb_id = await json_from_body(request, ["name", "imdb_id"]) # XXX restrict name @@ -435,7 +458,7 @@ async def add_user(request: Request) -> JSONResponse: @route("/users/{user_id}") @requires(["authenticated"]) -async def show_user(request: Request) -> JSONResponse: +async def show_user(request): user_id = as_ulid(request.path_params["user_id"]) if is_admin(request): @@ -464,7 +487,7 @@ async def show_user(request: Request) -> JSONResponse: @route("/users/{user_id}", methods=["DELETE"]) @requires(["authenticated", "admin"]) -async def remove_user(request: Request) -> JSONResponse: +async def remove_user(request): user_id = as_ulid(request.path_params["user_id"]) async with db.new_connection() as conn: @@ -482,7 +505,7 @@ async def remove_user(request: Request) -> JSONResponse: @route("/users/{user_id}", methods=["PATCH"]) @requires(["authenticated"]) -async def modify_user(request: Request) -> JSONResponse: +async def modify_user(request): user_id = as_ulid(request.path_params["user_id"]) if is_admin(request): @@ -529,13 +552,9 @@ async def modify_user(request: Request) -> JSONResponse: return JSONResponse(asplain(user)) -def is_valid_access(x: Any) -> TypeGuard[Access]: - return isinstance(x, str) and x in set("riw") - - @route("/users/{user_id}/groups", methods=["POST"]) @requires(["authenticated", "admin"]) -async def add_group_to_user(request: Request) -> JSONResponse: +async def add_group_to_user(request): user_id = as_ulid(request.path_params["user_id"]) async with db.new_connection() as conn: @@ -550,7 +569,7 @@ async def add_group_to_user(request: Request) -> JSONResponse: if not group: return not_found("Group not found") - if not is_valid_access(access): + if access not in set("riw"): raise HTTPException(422, "Invalid access level.") user.set_access(group_id, access) @@ -562,19 +581,19 @@ async def add_group_to_user(request: Request) -> JSONResponse: @route("/users/{user_id}/ratings") @requires(["private"]) -async def ratings_for_user(request: Request) -> JSONResponse: +async def ratings_for_user(request): not_implemented() @route("/users/{user_id}/ratings", methods=["PUT"]) @requires("authenticated") -async def set_rating_for_user(request: Request) -> JSONResponse: +async def set_rating_for_user(request): not_implemented() @route("/users/_reload_ratings", methods=["POST"]) @requires(["authenticated", "admin"]) -async def load_imdb_user_ratings(request: Request) -> JSONResponse: +async def load_imdb_user_ratings(request): ratings = [rating async for rating in imdb.refresh_user_ratings_from_imdb()] return JSONResponse({"new_ratings": [asplain(r) for r in ratings]}) @@ -582,7 +601,7 @@ async def load_imdb_user_ratings(request: Request) -> JSONResponse: @route("/groups") @requires(["authenticated", "admin"]) -async def list_groups(request: Request) -> JSONResponse: +async def list_groups(request): async with db.new_connection() as conn: groups = await db.get_all(conn, Group) @@ -591,7 +610,7 @@ async def list_groups(request: Request) -> JSONResponse: @route("/groups", methods=["POST"]) @requires(["authenticated", "admin"]) -async def add_group(request: Request) -> JSONResponse: +async def add_group(request): (name,) = await json_from_body(request, ["name"]) # XXX restrict name @@ -605,7 +624,7 @@ async def add_group(request: Request) -> JSONResponse: @route("/groups/{group_id}/users", methods=["POST"]) @requires(["authenticated"]) -async def add_user_to_group(request: Request) -> JSONResponse: +async def add_user_to_group(request): group_id = as_ulid(request.path_params["group_id"]) async with db.new_connection() as conn: group = await db.get(conn, Group, id=str(group_id)) @@ -641,12 +660,11 @@ async def add_user_to_group(request: Request) -> JSONResponse: return JSONResponse(asplain(group)) -async def http_exception(request: Request, exc: Exception) -> JSONResponse: - assert isinstance(exc, HTTPException) +async def http_exception(request, exc): return JSONResponse({"error": exc.detail}, status_code=exc.status_code) -def auth_error(conn: HTTPConnection, err: Exception) -> JSONResponse: +def auth_error(request, err): return unauthorized(str(err)) @@ -672,7 +690,7 @@ def create_app(): Mount(f"{config.api_base}v1", routes=_routes), ], middleware=[ - Middleware(ResponseTimeMiddleware, header_name="Unwind-Elapsed"), + Middleware(ResponseTimeMiddleware, header_name="unwind-elapsed"), Middleware( AuthenticationMiddleware, backend=BearerAuthBackend(config.api_credentials), diff --git a/unwind/web_models.py b/unwind/web_models.py index aba0d82..6e83e1d 100644 --- a/unwind/web_models.py +++ b/unwind/web_models.py @@ -1,22 +1,23 @@ from dataclasses import dataclass from typing import Container, Iterable -from . import imdb, models, types +from . import imdb, models -type URL = str +URL = str +Score100 = int # [0, 100] @dataclass class Rating: canonical_title: str - imdb_score: types.Score100 | None + imdb_score: Score100 | None imdb_votes: int | None media_type: str - movie_imdb_id: types.ImdbMovieId + movie_imdb_id: str original_title: str | None release_year: int - user_id: types.UserIdStr | None - user_score: types.Score100 | None + user_id: str | None + user_score: Score100 | None @classmethod def from_movie(cls, movie: models.Movie, *, rating: models.Rating | None = None): @@ -36,14 +37,13 @@ class Rating: @dataclass class RatingAggregate: canonical_title: str - imdb_score: types.Score100 | None + imdb_score: Score100 | None imdb_votes: int | None link: URL media_type: str original_title: str | None - user_scores: list[types.Score100] + user_scores: list[Score100] year: int - awards: list[str] @classmethod def from_movie(cls, movie: models.Movie, *, ratings: Iterable[models.Rating] = []): @@ -56,31 +56,15 @@ class RatingAggregate: original_title=movie.original_title, user_scores=[r.score for r in ratings], year=movie.release_year, - awards=[], ) -def _serialize_award(award: models.Award) -> str: - if award.category == "oscars": - return f"{award.category}:{award.name}" - elif award.category.startswith("imdb-"): - return f"{award.category}:{award.position}" - raise RuntimeError(f"Unsupported category: {award.category}") - - def aggregate_ratings( - ratings: Iterable[Rating], - user_ids: Container[types.UserIdStr], - *, - awards_dict: dict[types.ImdbMovieId, list[models.Award]] | None = None, + ratings: Iterable[Rating], user_ids: Container[str] ) -> Iterable[RatingAggregate]: - if awards_dict is None: - awards_dict = {} - - aggr: dict[types.ImdbMovieId, RatingAggregate] = {} + aggr: dict[str, RatingAggregate] = {} for r in ratings: - awards = awards_dict.get(r.movie_imdb_id, []) mov = aggr.setdefault( r.movie_imdb_id, RatingAggregate( @@ -92,7 +76,6 @@ def aggregate_ratings( original_title=r.original_title, user_scores=[], year=r.release_year, - awards=sorted(_serialize_award(a) for a in awards), ), ) # XXX do we need this? why don't we just get the ratings we're supposed to aggregate? diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 3c58fee..0000000 --- a/uv.lock +++ /dev/null @@ -1,518 +0,0 @@ -version = 1 -revision = 2 -requires-python = ">=3.13" - -[[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 = "alembic" -version = "1.15.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mako" }, - { name = "sqlalchemy" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e6/57/e314c31b261d1e8a5a5f1908065b4ff98270a778ce7579bd4254477209a7/alembic-1.15.2.tar.gz", hash = "sha256:1c72391bbdeffccfe317eefba686cb9a3c078005478885413b95c3b26c57a8a7", size = 1925573, upload-time = "2025-03-28T13:52:00.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/18/d89a443ed1ab9bcda16264716f809c663866d4ca8de218aa78fd50b38ead/alembic-1.15.2-py3-none-any.whl", hash = "sha256:2e76bd916d547f6900ec4bb5a90aeac1485d2c92536923d0b138c02b126edc53", size = 231911, upload-time = "2025-03-28T13:52:02.218Z" }, -] - -[[package]] -name = "anyio" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, -] - -[[package]] -name = "beautifulsoup4" -version = "4.13.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, -] - -[[package]] -name = "certifi" -version = "2025.4.26" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, -] - -[[package]] -name = "click" -version = "8.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" }, -] - -[[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 = "coverage" -version = "7.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" }, - { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" }, - { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" }, - { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" }, - { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" }, - { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" }, - { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" }, - { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" }, - { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" }, - { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" }, - { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" }, - { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" }, - { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" }, - { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" }, - { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" }, - { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, -] - -[[package]] -name = "greenlet" -version = "3.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/c1/a82edae11d46c0d83481aacaa1e578fea21d94a1ef400afd734d47ad95ad/greenlet-3.2.2.tar.gz", hash = "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485", size = 185797, upload-time = "2025-05-09T19:47:35.066Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/30/97b49779fff8601af20972a62cc4af0c497c1504dfbb3e93be218e093f21/greenlet-3.2.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59", size = 269150, upload-time = "2025-05-09T14:50:30.784Z" }, - { url = "https://files.pythonhosted.org/packages/21/30/877245def4220f684bc2e01df1c2e782c164e84b32e07373992f14a2d107/greenlet-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf", size = 637381, upload-time = "2025-05-09T15:24:12.893Z" }, - { url = "https://files.pythonhosted.org/packages/8e/16/adf937908e1f913856b5371c1d8bdaef5f58f251d714085abeea73ecc471/greenlet-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325", size = 651427, upload-time = "2025-05-09T15:24:51.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/49/6d79f58fa695b618654adac64e56aff2eeb13344dc28259af8f505662bb1/greenlet-3.2.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5", size = 645795, upload-time = "2025-05-09T15:29:26.673Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e6/28ed5cb929c6b2f001e96b1d0698c622976cd8f1e41fe7ebc047fa7c6dd4/greenlet-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825", size = 648398, upload-time = "2025-05-09T14:53:36.61Z" }, - { url = "https://files.pythonhosted.org/packages/9d/70/b200194e25ae86bc57077f695b6cc47ee3118becf54130c5514456cf8dac/greenlet-3.2.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d", size = 606795, upload-time = "2025-05-09T14:53:47.039Z" }, - { url = "https://files.pythonhosted.org/packages/f8/c8/ba1def67513a941154ed8f9477ae6e5a03f645be6b507d3930f72ed508d3/greenlet-3.2.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf", size = 1117976, upload-time = "2025-05-09T15:27:06.542Z" }, - { url = "https://files.pythonhosted.org/packages/c3/30/d0e88c1cfcc1b3331d63c2b54a0a3a4a950ef202fb8b92e772ca714a9221/greenlet-3.2.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708", size = 1145509, upload-time = "2025-05-09T14:54:02.223Z" }, - { url = "https://files.pythonhosted.org/packages/90/2e/59d6491834b6e289051b252cf4776d16da51c7c6ca6a87ff97e3a50aa0cd/greenlet-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421", size = 296023, upload-time = "2025-05-09T14:53:24.157Z" }, - { url = "https://files.pythonhosted.org/packages/65/66/8a73aace5a5335a1cba56d0da71b7bd93e450f17d372c5b7c5fa547557e9/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418", size = 629911, upload-time = "2025-05-09T15:24:22.376Z" }, - { url = "https://files.pythonhosted.org/packages/48/08/c8b8ebac4e0c95dcc68ec99198842e7db53eda4ab3fb0a4e785690883991/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4", size = 635251, upload-time = "2025-05-09T15:24:52.205Z" }, - { url = "https://files.pythonhosted.org/packages/37/26/7db30868f73e86b9125264d2959acabea132b444b88185ba5c462cb8e571/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763", size = 632620, upload-time = "2025-05-09T15:29:28.051Z" }, - { url = "https://files.pythonhosted.org/packages/10/ec/718a3bd56249e729016b0b69bee4adea0dfccf6ca43d147ef3b21edbca16/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b", size = 628851, upload-time = "2025-05-09T14:53:38.472Z" }, - { url = "https://files.pythonhosted.org/packages/9b/9d/d1c79286a76bc62ccdc1387291464af16a4204ea717f24e77b0acd623b99/greenlet-3.2.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207", size = 593718, upload-time = "2025-05-09T14:53:48.313Z" }, - { url = "https://files.pythonhosted.org/packages/cd/41/96ba2bf948f67b245784cd294b84e3d17933597dffd3acdb367a210d1949/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8", size = 1105752, upload-time = "2025-05-09T15:27:08.217Z" }, - { url = "https://files.pythonhosted.org/packages/68/3b/3b97f9d33c1f2eb081759da62bd6162159db260f602f048bc2f36b4c453e/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51", size = 1125170, upload-time = "2025-05-09T14:54:04.082Z" }, - { url = "https://files.pythonhosted.org/packages/31/df/b7d17d66c8d0f578d2885a3d8f565e9e4725eacc9d3fdc946d0031c055c4/greenlet-3.2.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", size = 269899, upload-time = "2025-05-09T14:54:01.581Z" }, -] - -[[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 = "honcho" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/c8/d860888358bf5c8a6e7d78d1b508b59b0e255afd5655f243b8f65166dafd/honcho-2.0.0.tar.gz", hash = "sha256:af3815c03c634bf67d50f114253ea9fef72ecff26e4fd06b29234789ac5b8b2e", size = 45618, upload-time = "2024-10-06T14:26:53.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/1c/25631fc359955569e63f5446dbb7022c320edf9846cbe892ee5113433a7e/honcho-2.0.0-py3-none-any.whl", hash = "sha256:56dcd04fc72d362a4befb9303b1a1a812cba5da283526fbc6509be122918ddf3", size = 22093, upload-time = "2024-10-06T14:26:52.181Z" }, -] - -[[package]] -name = "html5lib" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215, upload-time = "2020-06-22T23:32:38.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/dd/a834df6482147d48e225a49515aabc28974ad5a4ca3215c18a882565b028/html5lib-1.1-py2.py3-none-any.whl", hash = "sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d", size = 112173, upload-time = "2020-06-22T23:32:36.781Z" }, -] - -[[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 = "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 = "mako" -version = "1.3.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, -] - -[[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 = "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 = "pyright" -version = "1.1.400" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546, upload-time = "2025-04-24T12:55:18.907Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460, upload-time = "2025-04-24T12:55:17.002Z" }, -] - -[[package]] -name = "pytest" -version = "8.3.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, -] - -[[package]] -name = "pytest-asyncio" -version = "0.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, -] - -[[package]] -name = "pytest-cov" -version = "6.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, -] - -[[package]] -name = "ruff" -version = "0.11.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/4c/4a3c5a97faaae6b428b336dcca81d03ad04779f8072c267ad2bd860126bf/ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6", size = 4165632, upload-time = "2025-05-15T14:08:56.76Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/9f/596c628f8824a2ce4cd12b0f0b4c0629a62dfffc5d0f742c19a1d71be108/ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58", size = 10316243, upload-time = "2025-05-15T14:08:12.884Z" }, - { url = "https://files.pythonhosted.org/packages/3c/38/c1e0b77ab58b426f8c332c1d1d3432d9fc9a9ea622806e208220cb133c9e/ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed", size = 11083636, upload-time = "2025-05-15T14:08:16.551Z" }, - { url = "https://files.pythonhosted.org/packages/23/41/b75e15961d6047d7fe1b13886e56e8413be8467a4e1be0a07f3b303cd65a/ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca", size = 10441624, upload-time = "2025-05-15T14:08:19.032Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2c/e396b6703f131406db1811ea3d746f29d91b41bbd43ad572fea30da1435d/ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2", size = 10624358, upload-time = "2025-05-15T14:08:21.542Z" }, - { url = "https://files.pythonhosted.org/packages/bd/8c/ee6cca8bdaf0f9a3704796022851a33cd37d1340bceaf4f6e991eb164e2e/ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5", size = 10176850, upload-time = "2025-05-15T14:08:23.682Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ce/4e27e131a434321b3b7c66512c3ee7505b446eb1c8a80777c023f7e876e6/ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641", size = 11759787, upload-time = "2025-05-15T14:08:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/58/de/1e2e77fc72adc7cf5b5123fd04a59ed329651d3eab9825674a9e640b100b/ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947", size = 12430479, upload-time = "2025-05-15T14:08:28.013Z" }, - { url = "https://files.pythonhosted.org/packages/07/ed/af0f2340f33b70d50121628ef175523cc4c37619e98d98748c85764c8d88/ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4", size = 11919760, upload-time = "2025-05-15T14:08:30.956Z" }, - { url = "https://files.pythonhosted.org/packages/24/09/d7b3d3226d535cb89234390f418d10e00a157b6c4a06dfbe723e9322cb7d/ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f", size = 14041747, upload-time = "2025-05-15T14:08:33.297Z" }, - { url = "https://files.pythonhosted.org/packages/62/b3/a63b4e91850e3f47f78795e6630ee9266cb6963de8f0191600289c2bb8f4/ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b", size = 11550657, upload-time = "2025-05-15T14:08:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/46/63/a4f95c241d79402ccdbdb1d823d156c89fbb36ebfc4289dce092e6c0aa8f/ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2", size = 10489671, upload-time = "2025-05-15T14:08:38.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9b/c2238bfebf1e473495659c523d50b1685258b6345d5ab0b418ca3f010cd7/ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523", size = 10160135, upload-time = "2025-05-15T14:08:41.247Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ef/ba7251dd15206688dbfba7d413c0312e94df3b31b08f5d695580b755a899/ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125", size = 11170179, upload-time = "2025-05-15T14:08:43.762Z" }, - { url = "https://files.pythonhosted.org/packages/73/9f/5c336717293203ba275dbfa2ea16e49b29a9fd9a0ea8b6febfc17e133577/ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad", size = 11626021, upload-time = "2025-05-15T14:08:46.451Z" }, - { url = "https://files.pythonhosted.org/packages/d9/2b/162fa86d2639076667c9aa59196c020dc6d7023ac8f342416c2f5ec4bda0/ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19", size = 10494958, upload-time = "2025-05-15T14:08:49.601Z" }, - { url = "https://files.pythonhosted.org/packages/24/f3/66643d8f32f50a4b0d09a4832b7d919145ee2b944d43e604fbd7c144d175/ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224", size = 11650285, upload-time = "2025-05-15T14:08:52.392Z" }, - { url = "https://files.pythonhosted.org/packages/95/3a/2e8704d19f376c799748ff9cb041225c1d59f3e7711bc5596c8cfdc24925/ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1", size = 10765278, upload-time = "2025-05-15T14:08:54.56Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[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.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.41" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, - { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, - { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, - { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, - { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, -] - -[package.optional-dependencies] -aiosqlite = [ - { name = "aiosqlite" }, - { name = "greenlet" }, - { name = "typing-extensions" }, -] - -[[package]] -name = "starlette" -version = "0.46.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, -] - -[[package]] -name = "ulid-py" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/53/d14a8ec344048e21431821cb49e9a6722384f982b889c2dd449428dbdcc1/ulid-py-1.1.0.tar.gz", hash = "sha256:dc6884be91558df077c3011b9fb0c87d1097cb8fc6534b11f310161afd5738f0", size = 22514, upload-time = "2020-09-15T15:35:09.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/7c/a12c879fe6c2b136a718c142115ff99397fbf62b4929d970d58ae386d55f/ulid_py-1.1.0-py2.py3-none-any.whl", hash = "sha256:b56a0f809ef90d6020b21b89a87a48edc7c03aea80e5ed5174172e82d76e3987", size = 25753, upload-time = "2020-09-15T15:35:08.075Z" }, -] - -[[package]] -name = "unwind" -version = "0" -source = { editable = "." } -dependencies = [ - { name = "alembic" }, - { name = "beautifulsoup4" }, - { name = "html5lib" }, - { name = "httpx" }, - { name = "sqlalchemy", extra = ["aiosqlite"] }, - { name = "starlette" }, - { name = "ulid-py" }, - { name = "uvicorn" }, -] - -[package.dev-dependencies] -dev = [ - { name = "honcho" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "alembic", specifier = ">=1.13.1,<2" }, - { name = "beautifulsoup4", specifier = ">=4.9.3,<5" }, - { name = "html5lib", specifier = "~=1.1" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "sqlalchemy", extras = ["aiosqlite"], specifier = "~=2.0" }, - { name = "starlette", specifier = ">=0.46.2" }, - { name = "ulid-py", specifier = ">=1.1.0,<2" }, - { name = "uvicorn", specifier = ">=0.34.2" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "honcho" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, - { name = "ruff" }, -] - -[[package]] -name = "uvicorn" -version = "0.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, -] - -[[package]] -name = "webencodings" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, -]