diff --git a/.gitignore b/.gitignore index ba432e1..578559d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,5 @@ *.pyc /.cache /.pytest_cache +/build /data/* -/requirements.txt diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/Dockerfile b/Dockerfile index 7be9b6c..00b6a2c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/python:3.11-alpine +FROM docker.io/library/python:3.12-alpine RUN apk update --no-cache \ && apk upgrade --no-cache \ diff --git a/poetry.lock b/poetry.lock index 8ff7d02..bbd1764 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,27 +1,29 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiosqlite" -version = "0.18.0" +version = "0.19.0" description = "asyncio bridge to the standard sqlite3 module" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "aiosqlite-0.18.0-py3-none-any.whl", hash = "sha256:c3511b841e3a2c5614900ba1d179f366826857586f78abd75e7cbeb88e75a557"}, - {file = "aiosqlite-0.18.0.tar.gz", hash = "sha256:faa843ef5fb08bafe9a9b3859012d3d9d6f77ce3637899de20606b7fc39aa213"}, + {file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"}, + {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"}, ] +[package.extras] +dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] +docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] + [[package]] name = "anyio" -version = "3.6.2" +version = "4.1.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.8" files = [ - {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, - {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, + {file = "anyio-4.1.0-py3-none-any.whl", hash = "sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f"}, + {file = "anyio-4.1.0.tar.gz", hash = "sha256:5a0bec7085176715be77df87fc66d6c9d70626bd752fcc85f57cdbee5b3760da"}, ] [package.dependencies] @@ -29,54 +31,19 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16,<0.22)"] - -[[package]] -name = "attrs" -version = "22.2.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, - {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, -] - -[package.extras] -cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] -tests = ["attrs[tests-no-zope]", "zope.interface"] -tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] - -[[package]] -name = "autoflake" -version = "2.0.2" -description = "Removes unused imports and unused variables" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "autoflake-2.0.2-py3-none-any.whl", hash = "sha256:a82d8efdcbbb7129a8a23238c529fb9d9919c562e26bb7963ea6890fbfff7d02"}, - {file = "autoflake-2.0.2.tar.gz", hash = "sha256:e0164421ff13f805f08a023e249d84200bd00463d213b490906bfefa67e83830"}, -] - -[package.dependencies] -pyflakes = ">=3.0.0" +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.11.2" +version = "4.12.2" description = "Screen-scraping library" -category = "main" optional = false python-versions = ">=3.6.0" files = [ - {file = "beautifulsoup4-4.11.2-py3-none-any.whl", hash = "sha256:0e79446b10b3ecb499c1556f7e228a53e64a2bfcebd455f370d8927cb5b59e39"}, - {file = "beautifulsoup4-4.11.2.tar.gz", hash = "sha256:bc4bdda6717de5a2987436fb8d72f45dc90dd856bdfd512a1314ce90349a0106"}, + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, ] [package.dependencies] @@ -86,76 +53,26 @@ soupsieve = ">1.2" html5lib = ["html5lib"] lxml = ["lxml"] -[[package]] -name = "black" -version = "23.1.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, - {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, - {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, - {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, - {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, - {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, - {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, - {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, - {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, - {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, - {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, - {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, - {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, - {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, - {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, - {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, - {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, - {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "certifi" -version = "2022.12.7" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] [[package]] name = "click" -version = "8.1.3" +version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -165,7 +82,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -174,110 +90,143 @@ files = [ ] [[package]] -name = "databases" -version = "0.7.0" -description = "Async database support for Python." -category = "main" +name = "coverage" +version = "7.3.2" +description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "databases-0.7.0-py3-none-any.whl", hash = "sha256:cf5da4b8a3e3cd038c459529725ebb64931cbbb7a091102664f20ef8f6cefd0d"}, - {file = "databases-0.7.0.tar.gz", hash = "sha256:ea2d419d3d2eb80595b7ceb8f282056f080af62efe2fb9bcd83562f93ec4b674"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, + {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, + {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, + {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, + {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, + {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, + {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, + {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, + {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, + {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, + {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, + {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, + {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, + {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, + {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, + {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, + {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, + {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, + {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, + {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, + {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, + {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, + {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, + {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, + {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, + {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, + {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, + {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, ] -[package.dependencies] -aiosqlite = {version = "*", optional = true, markers = "extra == \"sqlite\""} -sqlalchemy = ">=1.4.42,<1.5" - [package.extras] -aiomysql = ["aiomysql"] -aiopg = ["aiopg"] -aiosqlite = ["aiosqlite"] -asyncmy = ["asyncmy"] -asyncpg = ["asyncpg"] -mysql = ["aiomysql"] -postgresql = ["asyncpg"] -sqlite = ["aiosqlite"] +toml = ["tomli"] [[package]] name = "greenlet" -version = "2.0.2" +version = "3.0.1" description = "Lightweight in-process concurrent programming" -category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +python-versions = ">=3.7" files = [ - {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, - {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, - {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, - {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, - {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, - {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, - {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, - {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, - {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, - {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, - {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, - {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, - {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, - {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, - {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, - {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, - {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, - {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, - {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, - {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, - {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, - {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, + {file = "greenlet-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63"}, + {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e"}, + {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846"}, + {file = "greenlet-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9"}, + {file = "greenlet-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234"}, + {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884"}, + {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94"}, + {file = "greenlet-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c"}, + {file = "greenlet-3.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5"}, + {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d"}, + {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445"}, + {file = "greenlet-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de"}, + {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166"}, + {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36"}, + {file = "greenlet-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1"}, + {file = "greenlet-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8"}, + {file = "greenlet-3.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9"}, + {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e"}, + {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a"}, + {file = "greenlet-3.0.1-cp38-cp38-win32.whl", hash = "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd"}, + {file = "greenlet-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6"}, + {file = "greenlet-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d"}, + {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8"}, + {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546"}, + {file = "greenlet-3.0.1-cp39-cp39-win32.whl", hash = "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57"}, + {file = "greenlet-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619"}, + {file = "greenlet-3.0.1.tar.gz", hash = "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b"}, ] [package.extras] -docs = ["Sphinx", "docutils (<0.18)"] +docs = ["Sphinx"] test = ["objgraph", "psutil"] [[package]] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -285,11 +234,27 @@ files = [ {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" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -309,67 +274,63 @@ lxml = ["lxml"] [[package]] name = "httpcore" -version = "0.16.3" +version = "0.17.3" description = "A minimal low-level HTTP client." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"}, - {file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"}, + {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, + {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, ] [package.dependencies] anyio = ">=3.0,<5.0" certifi = "*" h11 = ">=0.13,<0.15" -sniffio = ">=1.0.0,<2.0.0" +sniffio = "==1.*" [package.extras] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "httpx" -version = "0.23.3" +version = "0.24.1" description = "The next generation HTTP client." -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"}, - {file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"}, + {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, + {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, ] [package.dependencies] certifi = "*" -httpcore = ">=0.15.0,<0.17.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +httpcore = ">=0.15.0,<0.18.0" +idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -377,46 +338,15 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.12.0" -description = "A Python utility / library to sort Python imports." -category = "dev" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, - {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, -] - -[package.extras] -colors = ["colorama (>=0.4.3)"] -pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] -plugins = ["setuptools"] -requirements-deprecated-finder = ["pip-api", "pipreqs"] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "nodeenv" -version = "1.7.0" +version = "1.8.0" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, + {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] @@ -424,82 +354,39 @@ setuptools = "*" [[package]] name = "packaging" -version = "23.0" +version = "23.2" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, - {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "pathspec" -version = "0.11.1" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, -] - -[[package]] -name = "platformdirs" -version = "3.1.1" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "platformdirs-3.1.1-py3-none-any.whl", hash = "sha256:e5986afb596e4bb5bde29a79ac9061aa955b94fca2399b7aaac4090860920dd8"}, - {file = "platformdirs-3.1.1.tar.gz", hash = "sha256:024996549ee88ec1a9aa99ff7f8fc819bb59e2c3477b410d90a16d32d6e707aa"}, -] - -[package.extras] -docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] - [[package]] name = "pluggy" -version = "1.0.0" +version = "1.3.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "pyflakes" -version = "3.0.1" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pyflakes-3.0.1-py2.py3-none-any.whl", hash = "sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf"}, - {file = "pyflakes-3.0.1.tar.gz", hash = "sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"}, -] - [[package]] name = "pyright" -version = "1.1.299" +version = "1.1.337" description = "Command line wrapper for pyright" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.299-py3-none-any.whl", hash = "sha256:f34dfd0c2fcade34f9878b1fc69cb9456476dc78227e0a2fa046107ec55c0235"}, - {file = "pyright-1.1.299.tar.gz", hash = "sha256:b3a9a6affa1252c52793e8663ade59ff966f8495ecfad6328deffe59cfc5a9a9"}, + {file = "pyright-1.1.337-py3-none-any.whl", hash = "sha256:8cbd4ef71797258f816a8393a758c9c91213479f472082d0e3a735ef7ab5f65a"}, + {file = "pyright-1.1.337.tar.gz", hash = "sha256:81d81f839d1750385390c4c4a7b84b062ece2f9a078f87055d4d2a5914ef2a08"}, ] [package.dependencies] @@ -511,85 +398,106 @@ dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "7.2.2" +version = "7.4.3" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, - {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, ] [package.dependencies] -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.20.3" +version = "0.21.1" description = "Pytest support for asyncio" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-asyncio-0.20.3.tar.gz", hash = "sha256:83cbf01169ce3e8eb71c6c278ccb0574d1a7a3bb8eaaf5e50e0ad342afb33b36"}, - {file = "pytest_asyncio-0.20.3-py3-none-any.whl", hash = "sha256:f129998b209d04fcc65c96fc85c11e5316738358909a8399e93be553d7656442"}, + {file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"}, + {file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"}, ] [package.dependencies] -pytest = ">=6.1.0" +pytest = ">=7.0.0" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "main" -optional = false -python-versions = "*" -files = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} - -[package.extras] -idna2008 = ["idna"] - -[[package]] -name = "setuptools" -version = "67.6.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.6.0-py3-none-any.whl", hash = "sha256:b78aaa36f6b90a074c1fa651168723acbf45d14cb1196b6f02c0fd07f17623b2"}, - {file = "setuptools-67.6.0.tar.gz", hash = "sha256:2ee892cd5f29f3373097f5a814697e397cf3ce313616df0af11231e2ad118077"}, + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "ruff" +version = "0.1.6" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, + {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, + {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, + {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, + {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, +] + +[[package]] +name = "setuptools" +version = "69.0.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, + {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "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)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "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.1)", "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" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -601,7 +509,6 @@ files = [ name = "sniffio" version = "1.3.0" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -611,101 +518,112 @@ files = [ [[package]] name = "soupsieve" -version = "2.4" +version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "soupsieve-2.4-py3-none-any.whl", hash = "sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955"}, - {file = "soupsieve-2.4.tar.gz", hash = "sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"}, + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] [[package]] name = "sqlalchemy" -version = "1.4.46" +version = "2.0.23" description = "Database Abstraction Library" -category = "main" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-1.4.46-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:7001f16a9a8e06488c3c7154827c48455d1c1507d7228d43e781afbc8ceccf6d"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c7a46639ba058d320c9f53a81db38119a74b8a7a1884df44d09fbe807d028aaf"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27m-win32.whl", hash = "sha256:c04144a24103135ea0315d459431ac196fe96f55d3213bfd6d39d0247775c854"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27m-win_amd64.whl", hash = "sha256:7b81b1030c42b003fc10ddd17825571603117f848814a344d305262d370e7c34"}, - {file = "SQLAlchemy-1.4.46-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:939f9a018d2ad04036746e15d119c0428b1e557470361aa798e6e7d7f5875be0"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b7f4b6aa6e87991ec7ce0e769689a977776db6704947e562102431474799a857"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbf17ac9a61e7a3f1c7ca47237aac93cabd7f08ad92ac5b96d6f8dea4287fc1"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7f8267682eb41a0584cf66d8a697fef64b53281d01c93a503e1344197f2e01fe"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cb0ad8a190bc22d2112001cfecdec45baffdf41871de777239da6a28ed74b6"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-win32.whl", hash = "sha256:5f752676fc126edc1c4af0ec2e4d2adca48ddfae5de46bb40adbd3f903eb2120"}, - {file = "SQLAlchemy-1.4.46-cp310-cp310-win_amd64.whl", hash = "sha256:31de1e2c45e67a5ec1ecca6ec26aefc299dd5151e355eb5199cd9516b57340be"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d68e1762997bfebf9e5cf2a9fd0bcf9ca2fdd8136ce7b24bbd3bbfa4328f3e4a"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d112b0f3c1bc5ff70554a97344625ef621c1bfe02a73c5d97cac91f8cd7a41e"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69fac0a7054d86b997af12dc23f581cf0b25fb1c7d1fed43257dee3af32d3d6d"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-win32.whl", hash = "sha256:887865924c3d6e9a473dc82b70977395301533b3030d0f020c38fd9eba5419f2"}, - {file = "SQLAlchemy-1.4.46-cp311-cp311-win_amd64.whl", hash = "sha256:984ee13543a346324319a1fb72b698e521506f6f22dc37d7752a329e9cd00a32"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:9167d4227b56591a4cc5524f1b79ccd7ea994f36e4c648ab42ca995d28ebbb96"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d61e9ecc849d8d44d7f80894ecff4abe347136e9d926560b818f6243409f3c86"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3ec187acf85984263299a3f15c34a6c0671f83565d86d10f43ace49881a82718"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9883f5fae4fd8e3f875adc2add69f8b945625811689a6c65866a35ee9c0aea23"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-win32.whl", hash = "sha256:535377e9b10aff5a045e3d9ada8a62d02058b422c0504ebdcf07930599890eb0"}, - {file = "SQLAlchemy-1.4.46-cp36-cp36m-win_amd64.whl", hash = "sha256:18cafdb27834fa03569d29f571df7115812a0e59fd6a3a03ccb0d33678ec8420"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:a1ad90c97029cc3ab4ffd57443a20fac21d2ec3c89532b084b073b3feb5abff3"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4847f4b1d822754e35707db913396a29d874ee77b9c3c3ef3f04d5a9a6209618"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c5a99282848b6cae0056b85da17392a26b2d39178394fc25700bcf967e06e97a"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4b1cc7835b39835c75cf7c20c926b42e97d074147c902a9ebb7cf2c840dc4e2"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-win32.whl", hash = "sha256:c522e496f9b9b70296a7675272ec21937ccfc15da664b74b9f58d98a641ce1b6"}, - {file = "SQLAlchemy-1.4.46-cp37-cp37m-win_amd64.whl", hash = "sha256:ae067ab639fa499f67ded52f5bc8e084f045d10b5ac7bb928ae4ca2b6c0429a5"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:e3c1808008124850115a3f7e793a975cfa5c8a26ceeeb9ff9cbb4485cac556df"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d164df3d83d204c69f840da30b292ac7dc54285096c6171245b8d7807185aa"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b33ffbdbbf5446cf36cd4cc530c9d9905d3c2fe56ed09e25c22c850cdb9fac92"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d94682732d1a0def5672471ba42a29ff5e21bb0aae0afa00bb10796fc1e28dd"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-win32.whl", hash = "sha256:f8cb80fe8d14307e4124f6fad64dfd87ab749c9d275f82b8b4ec84c84ecebdbe"}, - {file = "SQLAlchemy-1.4.46-cp38-cp38-win_amd64.whl", hash = "sha256:07e48cbcdda6b8bc7a59d6728bd3f5f574ffe03f2c9fb384239f3789c2d95c2e"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1b1e5e96e2789d89f023d080bee432e2fef64d95857969e70d3cadec80bd26f0"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3714e5b33226131ac0da60d18995a102a17dddd42368b7bdd206737297823ad"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:955162ad1a931fe416eded6bb144ba891ccbf9b2e49dc7ded39274dd9c5affc5"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6e4cb5c63f705c9d546a054c60d326cbde7421421e2d2565ce3e2eee4e1a01f"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-win32.whl", hash = "sha256:51e1ba2884c6a2b8e19109dc08c71c49530006c1084156ecadfaadf5f9b8b053"}, - {file = "SQLAlchemy-1.4.46-cp39-cp39-win_amd64.whl", hash = "sha256:315676344e3558f1f80d02535f410e80ea4e8fddba31ec78fe390eff5fb8f466"}, - {file = "SQLAlchemy-1.4.46.tar.gz", hash = "sha256:6913b8247d8a292ef8315162a51931e2b40ce91681f1b6f18f697045200c4a30"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-win32.whl", hash = "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-win_amd64.whl", hash = "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, + {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, + {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and platform_machine == \"aarch64\" or python_version >= \"3\" and platform_machine == \"ppc64le\" or python_version >= \"3\" and platform_machine == \"x86_64\" or python_version >= \"3\" and platform_machine == \"amd64\" or python_version >= \"3\" and platform_machine == \"AMD64\" or python_version >= \"3\" and platform_machine == \"win32\" or python_version >= \"3\" and platform_machine == \"WIN32\""} +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.2.0", optional = true, markers = "extra == \"aiosqlite\""} [package.extras] -aiomysql = ["aiomysql", "greenlet (!=0.4.17)"] +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)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"] +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)", "sqlalchemy2-stubs"] -mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=7)", "cx-oracle (>=7,<8)"] +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.16.6,!=1.29.0)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql", "pymysql (<1)"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] sqlcipher = ["sqlcipher3-binary"] [[package]] name = "starlette" -version = "0.26.1" +version = "0.30.0" description = "The little ASGI library that shines." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"}, - {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"}, + {file = "starlette-0.30.0-py3-none-any.whl", hash = "sha256:cb15a5dfbd8de70c999bd1ae4b7e1ba625d74520bc57b28cc4086c7969431f2d"}, + {file = "starlette-0.30.0.tar.gz", hash = "sha256:9cf6bd5f2fbc091c2f22701f9b7f7dfcbd304a567845cffbf89d706543fd2a03"}, ] [package.dependencies] @@ -714,11 +632,21 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + [[package]] name = "ulid-py" version = "1.1.0" description = "Universally Unique Lexicographically Sortable Identifier" -category = "main" optional = false python-versions = "*" files = [ @@ -728,14 +656,13 @@ files = [ [[package]] name = "uvicorn" -version = "0.21.1" +version = "0.23.2" description = "The lightning-fast ASGI server." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "uvicorn-0.21.1-py3-none-any.whl", hash = "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"}, - {file = "uvicorn-0.21.1.tar.gz", hash = "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032"}, + {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, + {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, ] [package.dependencies] @@ -749,7 +676,6 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" -category = "main" optional = false python-versions = "*" files = [ @@ -759,5 +685,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.11" -content-hash = "a43dcab0548fc3be276e10ff19fe108211e5bdc42a8a161c744eeb4d20b14294" +python-versions = "^3.12" +content-hash = "ba28f3acc8701a53b35b1c8ea15169e151c74c277bad095f52e19e3f65be9ed7" diff --git a/pyproject.toml b/pyproject.toml index 08c45e9..417764e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,44 +1,46 @@ [tool.poetry] name = "unwind" -version = "0.1.0" +version = "0" description = "" authors = ["ducklet "] license = "LOL" [tool.poetry.dependencies] -python = "^3.11" +python = "^3.12" beautifulsoup4 = "^4.9.3" html5lib = "^1.1" -starlette = "^0.26" +starlette = "^0.30" ulid-py = "^1.1.0" -databases = {extras = ["sqlite"], version = "^0.7.0"} -uvicorn = "^0.21" -httpx = "^0.23.3" +uvicorn = "^0.23" +httpx = "^0.24" +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] -autoflake = "*" pytest = "*" pyright = "*" -black = "*" -isort = "*" pytest-asyncio = "*" +pytest-cov = "*" +ruff = "*" +honcho = "*" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.pyright] -pythonVersion = "3.11" +pythonVersion = "3.12" -[tool.isort] -profile = "black" - -[tool.autoflake] -remove-duplicate-keys = true -remove-unused-variables = true -remove-all-unused-imports = true +[tool.ruff] +target-version = "py312" ignore-init-module-imports = true -ignore-pass-after-docstring = true +select = ["I", "F401", "F601", "F602", "F841"] diff --git a/scripts/dev b/scripts/dev index dee1b94..0381aa4 100755 --- a/scripts/dev +++ b/scripts/dev @@ -2,6 +2,12 @@ cd "$RUN_DIR" +# Make Uvicorn defaults explicit. +: "${API_PORT:=8000}" +: "${API_HOST:=127.0.0.1}" +export API_PORT +export API_HOST + [ -z "${DEBUG:-}" ] || set -x exec honcho start diff --git a/scripts/dev-server b/scripts/dev-server index 7d3e2ef..c99ac4b 100755 --- a/scripts/dev-server +++ b/scripts/dev-server @@ -4,4 +4,9 @@ cd "$RUN_DIR" [ -z "${DEBUG:-}" ] || set -x -exec uvicorn unwind:create_app --factory --reload +exec uvicorn \ + --host "$API_HOST" \ + --port "$API_PORT" \ + --reload \ + --factory \ + unwind:create_app diff --git a/scripts/docker-build b/scripts/docker-build new file mode 100755 index 0000000..8f4c093 --- /dev/null +++ b/scripts/docker-build @@ -0,0 +1,25 @@ +#!/bin/sh -eu + +: "${DOCKER_BIN:=docker}" + +cd "$RUN_DIR" + +builddir=build + +[ -z "${DEBUG:-}" ] || set -x + +mkdir -p "$builddir" + +poetry export \ + --with=build \ + --output="$builddir"/requirements.txt + +githash=$(git rev-parse --short HEAD) +today=$(date -u '+%Y.%m.%d') +version="${today}+${githash}" +echo "$version" >"$builddir"/version + +$DOCKER_BIN build \ + --pull \ + --tag "code.dumpr.org/ducklet/unwind":"$version" \ + . diff --git a/scripts/docker-run b/scripts/docker-run new file mode 100755 index 0000000..407b199 --- /dev/null +++ b/scripts/docker-run @@ -0,0 +1,18 @@ +#!/bin/sh -eu + +: "${DOCKER_BIN:=docker}" + +cd "$RUN_DIR" + +[ -z "${DEBUG:-}" ] || set -x + +version=$(cat build/version) + +$DOCKER_BIN run \ + --init \ + -it --rm \ + --read-only \ + --memory '500m' \ + --publish 127.0.0.1:8000:8000 \ + --volume "$RUN_DIR"/data:/data \ + "code.dumpr.org/ducklet/unwind":"$version" diff --git a/scripts/lint-py b/scripts/lint-py index 54cb0f2..33387ae 100755 --- a/scripts/lint-py +++ b/scripts/lint-py @@ -4,7 +4,7 @@ cd "$RUN_DIR" [ -z "${DEBUG:-}" ] || set -x -autoflake --quiet --check --recursive unwind tests -isort unwind tests -black unwind tests +ruff check --fix . ||: +ruff format . + pyright diff --git a/scripts/server b/scripts/server index 599cb7f..5f236ed 100755 --- a/scripts/server +++ b/scripts/server @@ -11,4 +11,5 @@ export UNWIND_PORT exec uvicorn \ --host 0.0.0.0 \ --port "$UNWIND_PORT" \ - --factory unwind:create_app + --factory \ + unwind:create_app diff --git a/scripts/tests b/scripts/tests index df8b5a0..5eefc57 100755 --- a/scripts/tests +++ b/scripts/tests @@ -6,10 +6,10 @@ dbfile="${UNWIND_DATA:-./data}/tests.sqlite" # Rollback in Databases is currently broken, so we have to rebuild the database # each time; see https://github.com/encode/databases/issues/403 -trap 'rm "$dbfile"' EXIT TERM INT QUIT +trap 'rm "$dbfile" "${dbfile}-shm" "${dbfile}-wal"' EXIT TERM INT QUIT [ -z "${DEBUG:-}" ] || set -x -SQLALCHEMY_WARN_20=1 \ +export SQLALCHEMY_WARN_20=1 # XXX remove when we switched to SQLAlchemy 2.0 UNWIND_STORAGE="$dbfile" \ - python -m pytest "$@" + python -m pytest --cov "$@" diff --git a/tests/conftest.py b/tests/conftest.py index e57d3e1..17ce01a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,16 +17,19 @@ def event_loop(): @pytest_asyncio.fixture(scope="session") async def shared_conn(): - c = db.shared_connection() - await c.connect() + """A database connection, ready to use.""" + await db.open_connection_pool() - await db.apply_db_patches(c) - yield c + async with db.new_connection() as c: + db._test_connection = c + yield c + db._test_connection = None - await c.disconnect() + await db.close_connection_pool() @pytest_asyncio.fixture -async def conn(shared_conn): - async with shared_conn.transaction(force_rollback=True): +async def conn(shared_conn: db.Connection): + """A transacted database connection, will be rolled back after use.""" + async with db.transacted(shared_conn, force_rollback=True): yield shared_conn diff --git a/tests/test_db.py b/tests/test_db.py index ac8e64b..3619497 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -4,155 +4,416 @@ import pytest from unwind import db, models, web_models +_movie_imdb_id = 1230000 -@pytest.mark.asyncio -async def test_add_and_get(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m1 = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt0000000", - genres={"genre-1"}, - ) - await db.add(m1) - m2 = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt0000001", - genres={"genre-1"}, - ) - await db.add(m2) - - assert m1 == await db.get(models.Movie, id=str(m1.id)) - assert m2 == await db.get(models.Movie, id=str(m2.id)) +def a_movie(**kwds) -> models.Movie: + global _movie_imdb_id + _movie_imdb_id += 1 + args = { + "title": "test movie", + "release_year": 2013, + "media_type": "Movie", + "imdb_id": f"tt{_movie_imdb_id}", + "genres": {"genre-1"}, + } | kwds + return models.Movie(**args) @pytest.mark.asyncio -async def test_find_ratings(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - m1 = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt0000000", - genres={"genre-1"}, +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() + await db.add(conn, m1) + + m2 = a_movie(release_year=m1.release_year + 1) + await db.add(conn, m2) + + assert None is await db.get(conn, models.Movie) + assert None is await db.get(conn, models.Movie, id="blerp") + assert m1 == await db.get(conn, models.Movie, id=str(m1.id)) + assert m2 == await db.get(conn, models.Movie, release_year=m2.release_year) + assert None is await db.get( + conn, models.Movie, id=str(m1.id), release_year=m2.release_year + ) + assert m2 == await db.get( + conn, models.Movie, id=str(m2.id), release_year=m2.release_year + ) + assert m1 == await db.get( + conn, + models.Movie, + media_type=m1.media_type, + order_by=(models.movies.c.release_year, "asc"), + ) + assert m2 == await db.get( + conn, + models.Movie, + media_type=m1.media_type, + order_by=(models.movies.c.release_year, "desc"), + ) + + +@pytest.mark.asyncio +async def test_get_all(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) + + m2 = a_movie(release_year=m1.release_year) + await db.add(conn, m2) + + m3 = a_movie(release_year=m1.release_year + 1) + await db.add(conn, m3) + + assert [] == list(await db.get_all(conn, models.Movie, id="blerp")) + assert [m1] == list(await db.get_all(conn, models.Movie, id=str(m1.id))) + assert [m1, m2] == list( + await db.get_all(conn, models.Movie, release_year=m1.release_year) + ) + assert [m1, m2, m3] == list(await db.get_all(conn, models.Movie)) + + +@pytest.mark.asyncio +async def test_get_many(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) + + m2 = a_movie(release_year=m1.release_year) + await db.add(conn, m2) + + m3 = a_movie(release_year=m1.release_year + 1) + await db.add(conn, m3) + + assert [] == list(await db.get_many(conn, models.Movie)), "selected nothing" + assert [m1] == list(await db.get_many(conn, models.Movie, id=[str(m1.id)])) + assert [m1] == list(await db.get_many(conn, models.Movie, id={str(m1.id)})) + assert [m1, m2] == list( + await db.get_many(conn, models.Movie, release_year=[m1.release_year]) + ) + assert [m1, m2, m3] == list( + await db.get_many( + conn, models.Movie, release_year=[m1.release_year, m3.release_year] ) - await db.add(m1) + ) - m2 = models.Movie( - title="it's anöther Movie, Part 2", - release_year=2015, - media_type="Movie", - imdb_id="tt0000001", - genres={"genre-2"}, + +@pytest.mark.asyncio +async def test_add_and_get(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) + + m2 = a_movie() + await db.add(conn, m2) + + assert m1 == await db.get(conn, models.Movie, id=str(m1.id)) + assert m2 == await db.get(conn, models.Movie, id=str(m2.id)) + + +@pytest.mark.asyncio +async def test_update(conn: db.Connection): + m = a_movie() + await db.add(conn, m) + + assert m == await db.get(conn, models.Movie, id=str(m.id)) + m.title += "something else" + assert m != await db.get(conn, models.Movie, id=str(m.id)) + + await db.update(conn, m) + assert m == await db.get(conn, models.Movie, id=str(m.id)) + + +@pytest.mark.asyncio +async def test_remove(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) + assert m1 == await db.get(conn, models.Movie, id=str(m1.id)) + + await db.remove(conn, m1) + assert None is await db.get(conn, models.Movie, id=str(m1.id)) + + +@pytest.mark.asyncio +async def test_find_ratings(conn: db.Connection): + m1 = a_movie( + title="test movie", + release_year=2013, + genres={"genre-1"}, + ) + await db.add(conn, m1) + + m2 = a_movie( + title="it's anöther Movie, Part 2", + release_year=2015, + genres={"genre-2"}, + ) + await db.add(conn, m2) + + m3 = a_movie( + title="movie it's, Part 3", + release_year=m2.release_year, + genres=m2.genres, + ) + await db.add(conn, m3) + + u1 = models.User( + imdb_id="u00001", + name="User1", + secret="secret1", + ) + await db.add(conn, u1) + + u2 = models.User( + imdb_id="u00002", + name="User2", + secret="secret2", + ) + await db.add(conn, u2) + + r1 = models.Rating( + movie_id=m2.id, + movie=m2, + user_id=u1.id, + user=u1, + score=66, + rating_date=datetime.now(), + ) + await db.add(conn, r1) + + r2 = models.Rating( + movie_id=m2.id, + movie=m2, + user_id=u2.id, + user=u2, + score=77, + rating_date=datetime.now(), + ) + await db.add(conn, r2) + + # --- + + rows = await db.find_ratings( + conn, + title=m1.title, + media_type=m1.media_type, + exact=True, + ignore_tv_episodes=True, + include_unrated=True, + yearcomp=("=", m1.release_year), + limit_rows=3, + user_ids=[], + ) + ratings = (web_models.Rating(**r) for r in rows) + assert (web_models.RatingAggregate.from_movie(m1),) == tuple( + web_models.aggregate_ratings(ratings, user_ids=[]) + ) + + rows = await db.find_ratings(conn, title="movie", include_unrated=False) + ratings = tuple(web_models.Rating(**r) for r in rows) + assert ( + web_models.Rating.from_movie(m2, rating=r1), + web_models.Rating.from_movie(m2, rating=r2), + ) == ratings + + rows = await db.find_ratings(conn, title="movie", include_unrated=True) + ratings = tuple(web_models.Rating(**r) for r in rows) + assert ( + web_models.Rating.from_movie(m1), + web_models.Rating.from_movie(m2, rating=r1), + web_models.Rating.from_movie(m2, rating=r2), + web_models.Rating.from_movie(m3), + ) == ratings + + aggr = web_models.aggregate_ratings(ratings, user_ids=[]) + assert tuple( + web_models.RatingAggregate.from_movie(m) for m in [m1, m2, m3] + ) == tuple(aggr) + + aggr = web_models.aggregate_ratings(ratings, user_ids=[str(u1.id)]) + assert ( + web_models.RatingAggregate.from_movie(m1), + web_models.RatingAggregate.from_movie(m2, ratings=[r1]), + web_models.RatingAggregate.from_movie(m3), + ) == tuple(aggr) + + aggr = web_models.aggregate_ratings(ratings, user_ids=[str(u1.id), str(u2.id)]) + assert ( + web_models.RatingAggregate.from_movie(m1), + web_models.RatingAggregate.from_movie(m2, ratings=[r1, r2]), + web_models.RatingAggregate.from_movie(m3), + ) == tuple(aggr) + + rows = await db.find_ratings(conn, title="movie", include_unrated=True) + ratings = (web_models.Rating(**r) for r in rows) + aggr = web_models.aggregate_ratings(ratings, user_ids=[]) + assert tuple( + web_models.RatingAggregate.from_movie(m) for m in [m1, m2, m3] + ) == tuple(aggr) + + rows = await db.find_ratings(conn, title="test", include_unrated=True) + ratings = tuple(web_models.Rating(**r) for r in rows) + assert (web_models.Rating.from_movie(m1),) == ratings + + +@pytest.mark.asyncio +async def test_ratings_for_movies(conn: db.Connection): + m1 = a_movie() + await db.add(conn, m1) + + m2 = a_movie() + await db.add(conn, m2) + + u1 = models.User( + imdb_id="u00001", + name="User1", + secret="secret1", + ) + await db.add(conn, u1) + + u2 = models.User( + imdb_id="u00002", + name="User2", + secret="secret2", + ) + await db.add(conn, u2) + + r1 = models.Rating( + movie_id=m2.id, + movie=m2, + user_id=u1.id, + user=u1, + score=66, + rating_date=datetime.now(), + ) + await db.add(conn, r1) + + # --- + + movie_ids = [m1.id] + user_ids = [] + assert tuple() == tuple( + await db.ratings_for_movies(conn, movie_ids=movie_ids, user_ids=user_ids) + ) + + movie_ids = [m2.id] + user_ids = [] + assert (r1,) == tuple( + await db.ratings_for_movies(conn, movie_ids=movie_ids, user_ids=user_ids) + ) + + movie_ids = [m2.id] + user_ids = [u2.id] + assert tuple() == tuple( + await db.ratings_for_movies(conn, movie_ids=movie_ids, user_ids=user_ids) + ) + + movie_ids = [m2.id] + user_ids = [u1.id] + assert (r1,) == tuple( + await db.ratings_for_movies(conn, movie_ids=movie_ids, user_ids=user_ids) + ) + + movie_ids = [m1.id, m2.id] + user_ids = [u1.id, u2.id] + assert (r1,) == tuple( + await db.ratings_for_movies(conn, movie_ids=movie_ids, user_ids=user_ids) + ) + + +@pytest.mark.asyncio +async def test_find_movies(conn: db.Connection): + m1 = a_movie(title="movie one") + await db.add(conn, m1) + + m2 = a_movie(title="movie two", imdb_score=33, release_year=m1.release_year + 1) + await db.add(conn, m2) + + u1 = models.User( + imdb_id="u00001", + name="User1", + secret="secret1", + ) + await db.add(conn, u1) + + u2 = models.User( + imdb_id="u00002", + name="User2", + secret="secret2", + ) + await db.add(conn, u2) + + r1 = models.Rating( + movie_id=m2.id, + movie=m2, + user_id=u1.id, + user=u1, + score=66, + rating_date=datetime.now(), + ) + await db.add(conn, r1) + + # --- + + assert () == tuple( + await db.find_movies(conn, title=m1.title, include_unrated=False) + ) + assert ((m1, []),) == tuple( + await db.find_movies(conn, title=m1.title, include_unrated=True) + ) + + assert ((m1, []),) == tuple( + await db.find_movies(conn, title="mo on", exact=False, include_unrated=True) + ) + assert ((m1, []),) == tuple( + await db.find_movies(conn, title="movie one", exact=True, include_unrated=True) + ) + assert () == tuple( + await db.find_movies(conn, title="mo on", exact=True, include_unrated=True) + ) + + assert ((m2, []),) == tuple( + await db.find_movies(conn, title="movie", exact=False, include_unrated=False) + ) + assert ((m2, []), (m1, [])) == tuple( + await db.find_movies(conn, title="movie", exact=False, include_unrated=True) + ) + + assert ((m1, []),) == tuple( + await db.find_movies( + conn, include_unrated=True, yearcomp=("=", m1.release_year) ) - await db.add(m2) - - m3 = models.Movie( - title="movie it's, Part 3", - release_year=2015, - media_type="Movie", - imdb_id="tt0000002", - genres={"genre-2"}, + ) + assert ((m2, []),) == tuple( + await db.find_movies( + conn, include_unrated=True, yearcomp=("=", m2.release_year) ) - await db.add(m3) - - u1 = models.User( - imdb_id="u00001", - name="User1", - secret="secret1", + ) + assert ((m1, []),) == tuple( + await db.find_movies( + conn, include_unrated=True, yearcomp=("<", m2.release_year) ) - await db.add(u1) - - u2 = models.User( - imdb_id="u00002", - name="User2", - secret="secret2", + ) + assert ((m2, []),) == tuple( + await db.find_movies( + conn, include_unrated=True, yearcomp=(">", m1.release_year) ) - await db.add(u2) + ) - r1 = models.Rating( - movie_id=m2.id, - movie=m2, - user_id=u1.id, - user=u1, - score=66, - rating_date=datetime.now(), - ) - await db.add(r1) + assert ((m2, []), (m1, [])) == tuple( + await db.find_movies(conn, include_unrated=True) + ) + assert ((m2, []),) == tuple( + await db.find_movies(conn, include_unrated=True, limit_rows=1) + ) + assert ((m1, []),) == tuple( + await db.find_movies(conn, include_unrated=True, skip_rows=1) + ) - r2 = models.Rating( - movie_id=m2.id, - movie=m2, - user_id=u2.id, - user=u2, - score=77, - rating_date=datetime.now(), - ) - await db.add(r2) - - # --- - - rows = await db.find_ratings( - title=m1.title, - media_type=m1.media_type, - exact=True, - ignore_tv_episodes=True, - include_unrated=True, - yearcomp=("=", m1.release_year), - limit_rows=3, - user_ids=[], - ) - ratings = (web_models.Rating(**r) for r in rows) - assert (web_models.RatingAggregate.from_movie(m1),) == tuple( - web_models.aggregate_ratings(ratings, user_ids=[]) - ) - - rows = await db.find_ratings(title="movie", include_unrated=False) - ratings = tuple(web_models.Rating(**r) for r in rows) - assert ( - web_models.Rating.from_movie(m2, rating=r1), - web_models.Rating.from_movie(m2, rating=r2), - ) == ratings - - rows = await db.find_ratings(title="movie", include_unrated=True) - ratings = tuple(web_models.Rating(**r) for r in rows) - assert ( - web_models.Rating.from_movie(m1), - web_models.Rating.from_movie(m2, rating=r1), - web_models.Rating.from_movie(m2, rating=r2), - web_models.Rating.from_movie(m3), - ) == ratings - - aggr = web_models.aggregate_ratings(ratings, user_ids=[]) - assert tuple( - web_models.RatingAggregate.from_movie(m) for m in [m1, m2, m3] - ) == tuple(aggr) - - aggr = web_models.aggregate_ratings(ratings, user_ids=[str(u1.id)]) - assert ( - web_models.RatingAggregate.from_movie(m1), - web_models.RatingAggregate.from_movie(m2, ratings=[r1]), - web_models.RatingAggregate.from_movie(m3), - ) == tuple(aggr) - - aggr = web_models.aggregate_ratings(ratings, user_ids=[str(u1.id), str(u2.id)]) - assert ( - web_models.RatingAggregate.from_movie(m1), - web_models.RatingAggregate.from_movie(m2, ratings=[r1, r2]), - web_models.RatingAggregate.from_movie(m3), - ) == tuple(aggr) - - rows = await db.find_ratings(title="movie", include_unrated=True) - ratings = (web_models.Rating(**r) for r in rows) - aggr = web_models.aggregate_ratings(ratings, user_ids=[]) - assert tuple( - web_models.RatingAggregate.from_movie(m) for m in [m1, m2, m3] - ) == tuple(aggr) - - rows = await db.find_ratings(title="test", include_unrated=True) - ratings = tuple(web_models.Rating(**r) for r in rows) - assert (web_models.Rating.from_movie(m1),) == ratings + assert ((m2, [r1]), (m1, [])) == tuple( + await db.find_movies(conn, include_unrated=True, user_ids=[u1.id, u2.id]) + ) diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..f4bd7b6 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,11 @@ +import pytest + +from unwind import models + + +@pytest.mark.parametrize("mapper", models.mapper_registry.mappers) +def test_fields(mapper): + """Test that models.fields() matches exactly all table columns.""" + dcfields = {f.name for f in models.fields(mapper.class_)} + mfields = {c.name for c in mapper.columns} + assert dcfields == mfields diff --git a/tests/test_web.py b/tests/test_web.py index 358c2a2..0444406 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -1,53 +1,243 @@ +from datetime import datetime + import pytest from starlette.testclient import TestClient -from unwind import create_app, db, imdb, models +from unwind import config, create_app, db, imdb, models app = create_app() +@pytest.fixture(scope="module") +def unauthorized_client() -> TestClient: + # https://www.starlette.io/testclient/ + return TestClient(app) + + +@pytest.fixture(scope="module") +def authorized_client() -> TestClient: + client = TestClient(app) + client.auth = "user1", "secret1" + return client + + +@pytest.fixture(scope="module") +def admin_client() -> TestClient: + client = TestClient(app) + for token in config.api_credentials.values(): + break + else: + raise RuntimeError("No bearer tokens configured.") + client.headers = {"Authorization": f"Bearer {token}"} + return client + + @pytest.mark.asyncio -async def test_app(shared_conn: db.Database): - async with shared_conn.transaction(force_rollback=True): - # https://www.starlette.io/testclient/ - client = TestClient(app) - response = client.get("/api/v1/movies") - assert response.status_code == 403 +async def test_get_ratings_for_group( + conn: db.Connection, unauthorized_client: TestClient +): + user = models.User( + imdb_id="ur12345678", + name="user-1", + secret="secret-1", + 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)) - client.auth = "user1", "secret1" + resp = unauthorized_client.get(path) + assert resp.status_code == 404, "Group does not exist (yet)" - response = client.get("/api/v1/movies") - assert response.status_code == 200 - assert response.json() == [] + await db.add(conn, user) + await db.add(conn, group) - m = models.Movie( - title="test movie", - release_year=2013, - media_type="Movie", - imdb_id="tt12345678", - genres={"genre-1"}, - ) - await db.add(m) + resp = unauthorized_client.get(path) + assert resp.status_code == 200 + assert resp.json() == [] - response = client.get("/api/v1/movies", params={"include_unrated": 1}) - assert response.status_code == 200 - assert response.json() == [{**models.asplain(m), "user_scores": []}] + movie = models.Movie( + title="test movie", + release_year=2013, + media_type="Movie", + imdb_id="tt12345678", + genres={"genre-1"}, + ) + await db.add(conn, movie) - m_plain = { - "canonical_title": m.title, - "imdb_score": m.imdb_score, - "imdb_votes": m.imdb_votes, - "link": imdb.movie_url(m.imdb_id), - "media_type": m.media_type, - "original_title": m.original_title, - "user_scores": [], - "year": m.release_year, - } + rating = models.Rating( + movie_id=movie.id, user_id=user.id, score=66, rating_date=datetime.now() + ) + await db.add(conn, rating) - response = client.get("/api/v1/movies", params={"imdb_id": m.imdb_id}) - assert response.status_code == 200 - assert response.json() == [m_plain] + rating_aggregate = { + "canonical_title": movie.title, + "imdb_score": movie.imdb_score, + "imdb_votes": movie.imdb_votes, + "link": imdb.movie_url(movie.imdb_id), + "media_type": movie.media_type, + "original_title": movie.original_title, + "user_scores": [rating.score], + "year": movie.release_year, + } - response = client.get("/api/v1/movies", params={"unwind_id": str(m.id)}) - assert response.status_code == 200 - assert response.json() == [m_plain] + resp = unauthorized_client.get(path) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + + filters = { + "imdb_id": movie.imdb_id, + "unwind_id": str(movie.id), + "title": movie.title, + "media_type": movie.media_type, + "year": movie.release_year, + } + for k, v in filters.items(): + resp = unauthorized_client.get(path, params={k: v}) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + + resp = unauthorized_client.get(path, params={"title": "no such thing"}) + assert resp.status_code == 200 + assert resp.json() == [] + + # Test "exact" query param. + resp = unauthorized_client.get( + path, params={"title": "test movie", "exact": "true"} + ) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + resp = unauthorized_client.get(path, params={"title": "te mo", "exact": "false"}) + assert resp.status_code == 200 + assert resp.json() == [rating_aggregate] + resp = unauthorized_client.get(path, params={"title": "te mo", "exact": "true"}) + assert resp.status_code == 200 + assert resp.json() == [] + + # XXX Test "ignore_tv_episodes" query param. + # XXX Test "include_unrated" query param. + # XXX Test "per_page" query param. + + +@pytest.mark.asyncio +async def test_list_movies( + conn: db.Connection, + unauthorized_client: TestClient, + authorized_client: TestClient, +): + path = app.url_path_for("list_movies") + response = unauthorized_client.get(path) + assert response.status_code == 403 + + response = authorized_client.get(path) + assert response.status_code == 200 + assert response.json() == [] + + m = models.Movie( + title="test movie", + release_year=2013, + media_type="Movie", + imdb_id="tt12345678", + genres={"genre-1"}, + ) + await db.add(conn, m) + + response = authorized_client.get(path, params={"include_unrated": 1}) + assert response.status_code == 200 + assert response.json() == [{**models.asplain(m), "user_scores": []}] + + m_plain = { + "canonical_title": m.title, + "imdb_score": m.imdb_score, + "imdb_votes": m.imdb_votes, + "link": imdb.movie_url(m.imdb_id), + "media_type": m.media_type, + "original_title": m.original_title, + "user_scores": [], + "year": m.release_year, + } + + response = authorized_client.get(path, params={"imdb_id": m.imdb_id}) + assert response.status_code == 200 + assert response.json() == [m_plain] + + response = authorized_client.get(path, params={"unwind_id": str(m.id)}) + assert response.status_code == 200 + assert response.json() == [m_plain] + + +@pytest.mark.asyncio +async def test_list_users( + conn: db.Connection, + unauthorized_client: TestClient, + authorized_client: TestClient, + admin_client: TestClient, +): + path = app.url_path_for("list_users") + response = unauthorized_client.get(path) + assert response.status_code == 403 + + response = authorized_client.get(path) + assert response.status_code == 403 + + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [] + + m = models.User( + imdb_id="ur12345678", + name="user-1", + secret="secret-1", + groups=[], + ) + await db.add(conn, m) + + m_plain = { + "groups": m.groups, + "id": m.id, + "imdb_id": m.imdb_id, + "name": m.name, + "secret": m.secret, + } + + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [m_plain] + + +@pytest.mark.asyncio +async def test_list_groups( + conn: db.Connection, + unauthorized_client: TestClient, + authorized_client: TestClient, + admin_client: TestClient, +): + path = app.url_path_for("list_groups") + response = unauthorized_client.get(path) + assert response.status_code == 403 + + response = authorized_client.get(path) + assert response.status_code == 403 + + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [] + + m = models.Group( + name="group-1", + users=[models.GroupUser(id="123", name="itsa-me")], + ) + await db.add(conn, m) + + m_plain = { + "users": m.users, + "id": m.id, + "name": m.name, + } + + response = admin_client.get(path) + assert response.status_code == 200 + assert response.json() == [m_plain] diff --git a/unwind-ui/package-lock.json b/unwind-ui/package-lock.json index 1b7995d..1232bbd 100644 --- a/unwind-ui/package-lock.json +++ b/unwind-ui/package-lock.json @@ -1,7 +1,7 @@ { "name": "unwind-ui", "version": "0.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -19,9 +19,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", - "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==", + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", "bin": { "parser": "bin/babel-parser.js" }, @@ -29,6 +29,27 @@ "node": ">=6.0.0" } }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, "node_modules/@vitejs/plugin-vue": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.10.2.tgz", @@ -42,106 +63,106 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.26.tgz", - "integrity": "sha512-N5XNBobZbaASdzY9Lga2D9Lul5vdCIOXvUMd6ThcN8zgqQhPKfCV+wfAJNNJKQkSHudnYRO2gEB+lp0iN3g2Tw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.26", + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", "estree-walker": "^2.0.2", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-dom": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.26.tgz", - "integrity": "sha512-smBfaOW6mQDxcT3p9TKT6mE22vjxjJL50GFVJiI0chXYGU/xzC05QRGrW3HHVuJrmLTLx5zBhsZ2dIATERbarg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", "dependencies": { - "@vue/compiler-core": "3.2.26", - "@vue/shared": "3.2.26" + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.26.tgz", - "integrity": "sha512-ePpnfktV90UcLdsDQUh2JdiTuhV0Skv2iYXxfNMOK/F3Q+2BO0AulcVcfoksOpTJGmhhfosWfMyEaEf0UaWpIw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", + "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.26", - "@vue/compiler-dom": "3.2.26", - "@vue/compiler-ssr": "3.2.26", - "@vue/reactivity-transform": "3.2.26", - "@vue/shared": "3.2.26", + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", "estree-walker": "^2.0.2", - "magic-string": "^0.25.7", + "magic-string": "^0.30.0", "postcss": "^8.1.10", - "source-map": "^0.6.1" + "source-map-js": "^1.0.2" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.26.tgz", - "integrity": "sha512-2mywLX0ODc4Zn8qBoA2PDCsLEZfpUGZcyoFRLSOjyGGK6wDy2/5kyDOWtf0S0UvtoyVq95OTSGIALjZ4k2q/ag==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", + "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", "dependencies": { - "@vue/compiler-dom": "3.2.26", - "@vue/shared": "3.2.26" + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" } }, "node_modules/@vue/reactivity": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.26.tgz", - "integrity": "sha512-h38bxCZLW6oFJVDlCcAiUKFnXI8xP8d+eO0pcDxx+7dQfSPje2AO6M9S9QO6MrxQB7fGP0DH0dYQ8ksf6hrXKQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", + "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", "dependencies": { - "@vue/shared": "3.2.26" + "@vue/shared": "3.3.4" } }, "node_modules/@vue/reactivity-transform": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.26.tgz", - "integrity": "sha512-XKMyuCmzNA7nvFlYhdKwD78rcnmPb7q46uoR00zkX6yZrUmcCQ5OikiwUEVbvNhL5hBJuvbSO95jB5zkUon+eQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", + "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", "dependencies": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.26", - "@vue/shared": "3.2.26", + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", "estree-walker": "^2.0.2", - "magic-string": "^0.25.7" + "magic-string": "^0.30.0" } }, "node_modules/@vue/runtime-core": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.26.tgz", - "integrity": "sha512-BcYi7qZ9Nn+CJDJrHQ6Zsmxei2hDW0L6AB4vPvUQGBm2fZyC0GXd/4nVbyA2ubmuhctD5RbYY8L+5GUJszv9mQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", + "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", "dependencies": { - "@vue/reactivity": "3.2.26", - "@vue/shared": "3.2.26" + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" } }, "node_modules/@vue/runtime-dom": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.26.tgz", - "integrity": "sha512-dY56UIiZI+gjc4e8JQBwAifljyexfVCkIAu/WX8snh8vSOt/gMSEGwPRcl2UpYpBYeyExV8WCbgvwWRNt9cHhQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", + "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", "dependencies": { - "@vue/runtime-core": "3.2.26", - "@vue/shared": "3.2.26", - "csstype": "^2.6.8" + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" } }, "node_modules/@vue/server-renderer": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.26.tgz", - "integrity": "sha512-Jp5SggDUvvUYSBIvYEhy76t4nr1vapY/FIFloWmQzn7UxqaHrrBpbxrqPcTrSgGrcaglj0VBp22BKJNre4aA1w==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", + "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", "dependencies": { - "@vue/compiler-ssr": "3.2.26", - "@vue/shared": "3.2.26" + "@vue/compiler-ssr": "3.3.4", + "@vue/shared": "3.3.4" }, "peerDependencies": { - "vue": "3.2.26" + "vue": "3.3.4" } }, "node_modules/@vue/shared": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.26.tgz", - "integrity": "sha512-vPV6Cq+NIWbH5pZu+V+2QHE9y1qfuTq49uNWw4f7FDEeZaDU2H2cx5jcUZOAKW7qTrUS4k6qZPbMy1x4N96nbA==" + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -161,7 +182,7 @@ "node_modules/binary": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", "dev": true, "dependencies": { "buffers": "~0.1.1", @@ -174,7 +195,7 @@ "node_modules/bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", "dev": true }, "node_modules/brace-expansion": { @@ -199,21 +220,21 @@ "node_modules/buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", "dev": true, "engines": { "node": ">=0.2.0" } }, "node_modules/bulma": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.3.tgz", - "integrity": "sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g==" + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz", + "integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ==" }, "node_modules/chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", "dev": true, "dependencies": { "traverse": ">=0.3.0 <0.4" @@ -225,7 +246,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "node_modules/core-util-is": { @@ -235,52 +256,75 @@ "dev": true }, "node_modules/csstype": { - "version": "2.6.19", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz", - "integrity": "sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", "dev": true, "dependencies": { "readable-stream": "^2.0.2" } }, "node_modules/esbuild": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz", - "integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" }, + "engines": { + "node": ">=12" + }, "optionalDependencies": { - "esbuild-android-arm64": "0.13.15", - "esbuild-darwin-64": "0.13.15", - "esbuild-darwin-arm64": "0.13.15", - "esbuild-freebsd-64": "0.13.15", - "esbuild-freebsd-arm64": "0.13.15", - "esbuild-linux-32": "0.13.15", - "esbuild-linux-64": "0.13.15", - "esbuild-linux-arm": "0.13.15", - "esbuild-linux-arm64": "0.13.15", - "esbuild-linux-mips64le": "0.13.15", - "esbuild-linux-ppc64le": "0.13.15", - "esbuild-netbsd-64": "0.13.15", - "esbuild-openbsd-64": "0.13.15", - "esbuild-sunos-64": "0.13.15", - "esbuild-windows-32": "0.13.15", - "esbuild-windows-64": "0.13.15", - "esbuild-windows-arm64": "0.13.15" + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, "node_modules/esbuild-android-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz", - "integrity": "sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", "cpu": [ "arm64" ], @@ -288,12 +332,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-darwin-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz", - "integrity": "sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", "cpu": [ "x64" ], @@ -301,12 +348,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-darwin-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz", - "integrity": "sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", "cpu": [ "arm64" ], @@ -314,12 +364,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-freebsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz", - "integrity": "sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", "cpu": [ "x64" ], @@ -327,12 +380,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-freebsd-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz", - "integrity": "sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", "cpu": [ "arm64" ], @@ -340,12 +396,15 @@ "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-32": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz", - "integrity": "sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", "cpu": [ "ia32" ], @@ -353,12 +412,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz", - "integrity": "sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", "cpu": [ "x64" ], @@ -366,12 +428,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-arm": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz", - "integrity": "sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", "cpu": [ "arm" ], @@ -379,12 +444,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz", - "integrity": "sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", "cpu": [ "arm64" ], @@ -392,12 +460,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-mips64le": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz", - "integrity": "sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", "cpu": [ "mips64el" ], @@ -405,12 +476,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-linux-ppc64le": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz", - "integrity": "sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", "cpu": [ "ppc64" ], @@ -418,12 +492,47 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-netbsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz", - "integrity": "sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", "cpu": [ "x64" ], @@ -431,12 +540,15 @@ "optional": true, "os": [ "netbsd" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-openbsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz", - "integrity": "sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", "cpu": [ "x64" ], @@ -444,12 +556,15 @@ "optional": true, "os": [ "openbsd" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-sunos-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz", - "integrity": "sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", "cpu": [ "x64" ], @@ -457,12 +572,15 @@ "optional": true, "os": [ "sunos" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-windows-32": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz", - "integrity": "sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", "cpu": [ "ia32" ], @@ -470,12 +588,15 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-windows-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz", - "integrity": "sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", "cpu": [ "x64" ], @@ -483,12 +604,15 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/esbuild-windows-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz", - "integrity": "sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", "cpu": [ "arm64" ], @@ -496,7 +620,10 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": ">=12" + } }, "node_modules/estree-walker": { "version": "2.0.2", @@ -506,7 +633,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, "node_modules/fsevents": { @@ -545,15 +672,15 @@ "dev": true }, "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -565,9 +692,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "node_modules/has": { @@ -585,7 +712,7 @@ "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "dependencies": { "once": "^1.3.0", @@ -599,9 +726,9 @@ "dev": true }, "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -613,27 +740,30 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true }, "node_modules/listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", "dev": true }, "node_modules/magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", + "integrity": "sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==", "dependencies": { - "sourcemap-codec": "^1.4.4" + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -643,27 +773,36 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, "dependencies": { - "minimist": "^1.2.5" + "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "node_modules/nanoid": { - "version": "3.1.32", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.32.tgz", - "integrity": "sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -674,7 +813,7 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "dependencies": { "wrappy": "1" @@ -683,7 +822,7 @@ "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -701,20 +840,30 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/postcss": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", - "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", + "version": "8.4.27", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", + "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "nanoid": "^3.1.30", + "nanoid": "^3.3.6", "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" + "source-map-js": "^1.0.2" }, "engines": { "node": "^10 || ^12 || >=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" } }, "node_modules/process-nextick-args": { @@ -724,9 +873,9 @@ "dev": true }, "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "dependencies": { "core-util-is": "~1.0.0", @@ -739,12 +888,12 @@ } }, "node_modules/resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "dependencies": { - "is-core-module": "^2.8.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -768,9 +917,9 @@ } }, "node_modules/rollup": { - "version": "2.63.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz", - "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==", + "version": "2.77.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz", + "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -791,30 +940,17 @@ "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", - "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "engines": { "node": ">=0.10.0" } }, - "node_modules/sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -839,16 +975,16 @@ "node_modules/traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", "dev": true, "engines": { "node": "*" } }, "node_modules/typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -879,19 +1015,19 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, "node_modules/vite": { - "version": "2.7.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.7.12.tgz", - "integrity": "sha512-KvPYToRQWhRfBeVkyhkZ5hASuHQkqZUUdUcE3xyYtq5oYEPIJ0h9LWiWTO6v990glmSac2cEPeYeXzpX5Z6qKQ==", + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz", + "integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==", "dev": true, "dependencies": { - "esbuild": "^0.13.12", - "postcss": "^8.4.5", - "resolve": "^1.20.0", - "rollup": "^2.59.0" + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": ">=2.59.0 <2.78.0" }, "bin": { "vite": "bin/vite.js" @@ -920,15 +1056,15 @@ } }, "node_modules/vue": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.26.tgz", - "integrity": "sha512-KD4lULmskL5cCsEkfhERVRIOEDrfEL9CwAsLYpzptOGjaGFNWo3BQ9g8MAb7RaIO71rmVOziZ/uEN/rHwcUIhg==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", + "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", "dependencies": { - "@vue/compiler-dom": "3.2.26", - "@vue/compiler-sfc": "3.2.26", - "@vue/runtime-dom": "3.2.26", - "@vue/server-renderer": "3.2.26", - "@vue/shared": "3.2.26" + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-sfc": "3.3.4", + "@vue/runtime-dom": "3.3.4", + "@vue/server-renderer": "3.3.4", + "@vue/shared": "3.3.4" } }, "node_modules/vue-tsc": { @@ -946,697 +1082,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - }, - "dependencies": { - "@babel/parser": { - "version": "7.16.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", - "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==" - }, - "@vitejs/plugin-vue": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.10.2.tgz", - "integrity": "sha512-/QJ0Z9qfhAFtKRY+r57ziY4BSbGUTGsPRMpB/Ron3QPwBZM4OZAZHdTa4a8PafCwU5DTatXG8TMDoP8z+oDqJw==", - "dev": true, - "requires": {} - }, - "@vue/compiler-core": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.26.tgz", - "integrity": "sha512-N5XNBobZbaASdzY9Lga2D9Lul5vdCIOXvUMd6ThcN8zgqQhPKfCV+wfAJNNJKQkSHudnYRO2gEB+lp0iN3g2Tw==", - "requires": { - "@babel/parser": "^7.16.4", - "@vue/shared": "3.2.26", - "estree-walker": "^2.0.2", - "source-map": "^0.6.1" - } - }, - "@vue/compiler-dom": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.26.tgz", - "integrity": "sha512-smBfaOW6mQDxcT3p9TKT6mE22vjxjJL50GFVJiI0chXYGU/xzC05QRGrW3HHVuJrmLTLx5zBhsZ2dIATERbarg==", - "requires": { - "@vue/compiler-core": "3.2.26", - "@vue/shared": "3.2.26" - } - }, - "@vue/compiler-sfc": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.26.tgz", - "integrity": "sha512-ePpnfktV90UcLdsDQUh2JdiTuhV0Skv2iYXxfNMOK/F3Q+2BO0AulcVcfoksOpTJGmhhfosWfMyEaEf0UaWpIw==", - "requires": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.26", - "@vue/compiler-dom": "3.2.26", - "@vue/compiler-ssr": "3.2.26", - "@vue/reactivity-transform": "3.2.26", - "@vue/shared": "3.2.26", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7", - "postcss": "^8.1.10", - "source-map": "^0.6.1" - } - }, - "@vue/compiler-ssr": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.26.tgz", - "integrity": "sha512-2mywLX0ODc4Zn8qBoA2PDCsLEZfpUGZcyoFRLSOjyGGK6wDy2/5kyDOWtf0S0UvtoyVq95OTSGIALjZ4k2q/ag==", - "requires": { - "@vue/compiler-dom": "3.2.26", - "@vue/shared": "3.2.26" - } - }, - "@vue/reactivity": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.26.tgz", - "integrity": "sha512-h38bxCZLW6oFJVDlCcAiUKFnXI8xP8d+eO0pcDxx+7dQfSPje2AO6M9S9QO6MrxQB7fGP0DH0dYQ8ksf6hrXKQ==", - "requires": { - "@vue/shared": "3.2.26" - } - }, - "@vue/reactivity-transform": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.26.tgz", - "integrity": "sha512-XKMyuCmzNA7nvFlYhdKwD78rcnmPb7q46uoR00zkX6yZrUmcCQ5OikiwUEVbvNhL5hBJuvbSO95jB5zkUon+eQ==", - "requires": { - "@babel/parser": "^7.16.4", - "@vue/compiler-core": "3.2.26", - "@vue/shared": "3.2.26", - "estree-walker": "^2.0.2", - "magic-string": "^0.25.7" - } - }, - "@vue/runtime-core": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.26.tgz", - "integrity": "sha512-BcYi7qZ9Nn+CJDJrHQ6Zsmxei2hDW0L6AB4vPvUQGBm2fZyC0GXd/4nVbyA2ubmuhctD5RbYY8L+5GUJszv9mQ==", - "requires": { - "@vue/reactivity": "3.2.26", - "@vue/shared": "3.2.26" - } - }, - "@vue/runtime-dom": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.26.tgz", - "integrity": "sha512-dY56UIiZI+gjc4e8JQBwAifljyexfVCkIAu/WX8snh8vSOt/gMSEGwPRcl2UpYpBYeyExV8WCbgvwWRNt9cHhQ==", - "requires": { - "@vue/runtime-core": "3.2.26", - "@vue/shared": "3.2.26", - "csstype": "^2.6.8" - } - }, - "@vue/server-renderer": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.26.tgz", - "integrity": "sha512-Jp5SggDUvvUYSBIvYEhy76t4nr1vapY/FIFloWmQzn7UxqaHrrBpbxrqPcTrSgGrcaglj0VBp22BKJNre4aA1w==", - "requires": { - "@vue/compiler-ssr": "3.2.26", - "@vue/shared": "3.2.26" - } - }, - "@vue/shared": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.26.tgz", - "integrity": "sha512-vPV6Cq+NIWbH5pZu+V+2QHE9y1qfuTq49uNWw4f7FDEeZaDU2H2cx5jcUZOAKW7qTrUS4k6qZPbMy1x4N96nbA==" - }, - "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 - }, - "big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "dev": true - }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", - "dev": true, - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "dev": true - }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", - "dev": true - }, - "bulma": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.3.tgz", - "integrity": "sha512-0d7GNW1PY4ud8TWxdNcP6Cc8Bu7MxcntD/RRLGWuiw/s0a9P+XlH/6QoOIrmbj6o8WWJzJYhytiu9nFjTszk1g==" - }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", - "dev": true, - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "csstype": { - "version": "2.6.19", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.19.tgz", - "integrity": "sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==" - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, - "esbuild": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.15.tgz", - "integrity": "sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==", - "dev": true, - "requires": { - "esbuild-android-arm64": "0.13.15", - "esbuild-darwin-64": "0.13.15", - "esbuild-darwin-arm64": "0.13.15", - "esbuild-freebsd-64": "0.13.15", - "esbuild-freebsd-arm64": "0.13.15", - "esbuild-linux-32": "0.13.15", - "esbuild-linux-64": "0.13.15", - "esbuild-linux-arm": "0.13.15", - "esbuild-linux-arm64": "0.13.15", - "esbuild-linux-mips64le": "0.13.15", - "esbuild-linux-ppc64le": "0.13.15", - "esbuild-netbsd-64": "0.13.15", - "esbuild-openbsd-64": "0.13.15", - "esbuild-sunos-64": "0.13.15", - "esbuild-windows-32": "0.13.15", - "esbuild-windows-64": "0.13.15", - "esbuild-windows-arm64": "0.13.15" - } - }, - "esbuild-android-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz", - "integrity": "sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==", - "dev": true, - "optional": true - }, - "esbuild-darwin-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz", - "integrity": "sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==", - "dev": true, - "optional": true - }, - "esbuild-darwin-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz", - "integrity": "sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz", - "integrity": "sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz", - "integrity": "sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==", - "dev": true, - "optional": true - }, - "esbuild-linux-32": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz", - "integrity": "sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==", - "dev": true, - "optional": true - }, - "esbuild-linux-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz", - "integrity": "sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz", - "integrity": "sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz", - "integrity": "sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==", - "dev": true, - "optional": true - }, - "esbuild-linux-mips64le": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz", - "integrity": "sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==", - "dev": true, - "optional": true - }, - "esbuild-linux-ppc64le": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz", - "integrity": "sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==", - "dev": true, - "optional": true - }, - "esbuild-netbsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz", - "integrity": "sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==", - "dev": true, - "optional": true - }, - "esbuild-openbsd-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz", - "integrity": "sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==", - "dev": true, - "optional": true - }, - "esbuild-sunos-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz", - "integrity": "sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==", - "dev": true, - "optional": true - }, - "esbuild-windows-32": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz", - "integrity": "sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==", - "dev": true, - "optional": true - }, - "esbuild-windows-64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz", - "integrity": "sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==", - "dev": true, - "optional": true - }, - "esbuild-windows-arm64": { - "version": "0.13.15", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz", - "integrity": "sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==", - "dev": true, - "optional": true - }, - "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==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-fs": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", - "dev": true - }, - "magic-string": { - "version": "0.25.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", - "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", - "requires": { - "sourcemap-codec": "^1.4.4" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "nanoid": { - "version": "3.1.32", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.32.tgz", - "integrity": "sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "postcss": { - "version": "8.4.5", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", - "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", - "requires": { - "nanoid": "^3.1.30", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.1" - } - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "resolve": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz", - "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==", - "dev": true, - "requires": { - "is-core-module": "^2.8.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "rollup": { - "version": "2.63.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.63.0.tgz", - "integrity": "sha512-nps0idjmD+NXl6OREfyYXMn/dar3WGcyKn+KBzPdaLecub3x/LrId0wUcthcr8oZUAcZAR8NKcfGGFlNgGL1kQ==", - "dev": true, - "requires": { - "fsevents": "~2.3.2" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz", - "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==" - }, - "sourcemap-codec": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", - "dev": true - }, - "typescript": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", - "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", - "dev": true - }, - "unzipper": { - "version": "0.10.11", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz", - "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==", - "dev": true, - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "vite": { - "version": "2.7.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.7.12.tgz", - "integrity": "sha512-KvPYToRQWhRfBeVkyhkZ5hASuHQkqZUUdUcE3xyYtq5oYEPIJ0h9LWiWTO6v990glmSac2cEPeYeXzpX5Z6qKQ==", - "dev": true, - "requires": { - "esbuild": "^0.13.12", - "fsevents": "~2.3.2", - "postcss": "^8.4.5", - "resolve": "^1.20.0", - "rollup": "^2.59.0" - } - }, - "vue": { - "version": "3.2.26", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.26.tgz", - "integrity": "sha512-KD4lULmskL5cCsEkfhERVRIOEDrfEL9CwAsLYpzptOGjaGFNWo3BQ9g8MAb7RaIO71rmVOziZ/uEN/rHwcUIhg==", - "requires": { - "@vue/compiler-dom": "3.2.26", - "@vue/compiler-sfc": "3.2.26", - "@vue/runtime-dom": "3.2.26", - "@vue/server-renderer": "3.2.26", - "@vue/shared": "3.2.26" - } - }, - "vue-tsc": { - "version": "0.0.24", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-0.0.24.tgz", - "integrity": "sha512-Qx0V7jkWMtvddtaWa1SA8YKkBCRmjq9zZUB2UIMZiso6JSH538oHD2VumSzkoDnAfFbY3t0/j1mB2abpA0bGWA==", - "dev": true, - "requires": { - "unzipper": "0.10.11" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true } } diff --git a/unwind-ui/vite.config.ts b/unwind-ui/vite.config.ts index 84d4068..99d078b 100644 --- a/unwind-ui/vite.config.ts +++ b/unwind-ui/vite.config.ts @@ -1,13 +1,29 @@ import { defineConfig } from "vite" import vue from "@vitejs/plugin-vue" +// Vite defaults. +const vite_host = "localhost" +const vite_port = 3000 + +const base = process.env.BASE_URL || "/" +const proxied_api_url = `http://${vite_host}:${vite_port}/api/` +const real_api_url = `http://${process.env.API_HOST}:${process.env.API_PORT}/api/` + // https://vitejs.dev/config/ export default defineConfig({ - base: process.env.BASE_URL || "/", + base, define: { - "process.env.API_URL": JSON.stringify( - process.env.API_URL || "http://localhost:8000/api/", - ), + "process.env.API_URL": JSON.stringify(process.env.API_URL || proxied_api_url), + }, + server: { + host: vite_host, + port: vite_port, + proxy: { + [`${base}api`]: { + target: real_api_url, + prependPath: false, + }, + }, }, plugins: [vue()], }) diff --git a/unwind/config.py b/unwind/config.py index 6cc255a..9e3a908 100644 --- a/unwind/config.py +++ b/unwind/config.py @@ -2,20 +2,20 @@ import os import tomllib from pathlib import Path -datadir = Path(os.getenv("UNWIND_DATA") or "./data") -cachedir = ( - Path(cachedir) - if (cachedir := os.getenv("UNWIND_CACHEDIR", datadir / ".cache")) - else None +datadir: Path = Path(os.getenv("UNWIND_DATA") or "./data") +cachedir: Path = Path(p) if (p := os.getenv("UNWIND_CACHEDIR")) else datadir / ".cache" +debug: bool = os.getenv("DEBUG") == "1" +loglevel: str = os.getenv("UNWIND_LOGLEVEL") or ("DEBUG" if debug else "INFO") +storage_path: Path = ( + Path(p) if (p := os.getenv("UNWIND_STORAGE")) else datadir / "db.sqlite" +) +config_path: Path = ( + Path(p) if (p := os.getenv("UNWIND_CONFIG")) else datadir / "config.toml" ) -debug = os.getenv("DEBUG") == "1" -loglevel = os.getenv("UNWIND_LOGLEVEL") or ("DEBUG" if debug else "INFO") -storage_path = os.getenv("UNWIND_STORAGE", datadir / "db.sqlite") -config_path = os.getenv("UNWIND_CONFIG", datadir / "config.toml") with open(config_path, "rb") as fd: _config = tomllib.load(fd) -api_base = _config["api"].get("base", "/api/") -api_cors = _config["api"].get("cors", "*") -api_credentials = _config["api"].get("credentials", {}) +api_base: str = _config["api"].get("base", "/api/") +api_cors: str = _config["api"].get("cors", "*") +api_credentials: dict[str, str] = _config["api"].get("credentials", {}) diff --git a/unwind/db.py b/unwind/db.py index c07b3a9..3ebca66 100644 --- a/unwind/db.py +++ b/unwind/db.py @@ -1,24 +1,27 @@ -import asyncio import contextlib import logging -import re -import threading from pathlib import Path -from typing import Any, Iterable, Literal, Type, TypeVar +from typing import Any, AsyncGenerator, Iterable, Literal, Sequence, Type, TypeVar -import sqlalchemy -from databases import Database +import sqlalchemy as sa +from sqlalchemy.dialects.sqlite import insert +from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine from . import config from .models import ( + Model, Movie, Progress, Rating, User, asplain, - fields, + db_patches, fromplain, + metadata, + movies, optional_fields, + progress, + ratings, utcnow, ) from .types import ULID @@ -26,7 +29,9 @@ from .types import ULID log = logging.getLogger(__name__) T = TypeVar("T") -_shared_connection: Database | None = None +_engine: AsyncEngine | None = None + +type Connection = AsyncConnection async def open_connection_pool() -> None: @@ -34,10 +39,13 @@ async def open_connection_pool() -> None: This function needs to be called before any access to the database can happen. """ - db = shared_connection() - await db.connect() + async with transaction() as conn: + await conn.execute(sa.text("PRAGMA journal_mode=WAL")) - await apply_db_patches(db) + 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: @@ -46,48 +54,33 @@ async def close_connection_pool() -> None: This function should be called before the app shuts down to ensure all data has been flushed to the database. """ - db = shared_connection() + engine = _shared_engine() - # Run automatic ANALYZE prior to closing the db, - # see https://sqlite.com/lang_analyze.html. - await db.execute("PRAGMA analysis_limit=400") - await db.execute("PRAGMA optimize") + async with engine.begin() as conn: + # Run automatic ANALYZE prior to closing the db, + # see https://sqlite.com/lang_analyze.html. + await conn.execute(sa.text("PRAGMA analysis_limit=400")) + await conn.execute(sa.text("PRAGMA optimize")) - await db.disconnect() + await engine.dispose() -async def _create_patch_db(db): - query = """ - CREATE TABLE IF NOT EXISTS db_patches ( - id INTEGER PRIMARY KEY, - current TEXT - ) - """ - await db.execute(query) - - -async def current_patch_level(db) -> str: - await _create_patch_db(db) - - query = "SELECT current FROM db_patches" - current = await db.fetch_val(query) +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(db, current: str): - await _create_patch_db(db) - - query = """ - INSERT INTO db_patches VALUES (1, :current) - ON CONFLICT DO UPDATE SET current=excluded.current - """ - await db.execute(query, values={"current": current}) +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(db: Database): +async def apply_db_patches(conn: Connection, /) -> None: """Apply all remaining patches to the database. Beware that patches will be applied in lexicographical order, @@ -99,7 +92,7 @@ async def apply_db_patches(db: Database): using two consecutive semi-colons (;). Failing to do so will result in an error. """ - applied_lvl = await current_patch_level(db) + applied_lvl = await current_patch_level(conn) did_patch = False @@ -118,29 +111,52 @@ async def apply_db_patches(db: Database): ) raise RuntimeError("No statement found.") - async with db.transaction(): + async with transacted(conn): for query in queries: - await db.execute(query) + await conn.execute(sa.text(query)) - await set_current_patch_level(db, patch_lvl) + await set_current_patch_level(conn, patch_lvl) did_patch = True if did_patch: - await db.execute("vacuum") + await _vacuum(conn) -async def get_import_progress() -> Progress | None: +async def _vacuum(conn: Connection, /) -> None: + """Vacuum the database. + + This function cannot be run on a connection with an open transaction. + """ + # With SQLAlchemy's "autobegin" behavior we need to switch the connection + # to "autocommit" first to keep it from automatically starting a transaction, + # as VACUUM cannot be run inside a transaction for most databases. + await conn.commit() + isolation_level = await conn.get_isolation_level() + log.debug("Previous isolation_level: %a", isolation_level) + await conn.execution_options(isolation_level="AUTOCOMMIT") + try: + await conn.execute(sa.text("vacuum")) + await conn.commit() + finally: + await conn.execution_options(isolation_level=isolation_level) + + +async def get_import_progress(conn: Connection, /) -> Progress | None: """Return the latest import progress.""" - return await get(Progress, type="import-imdb-movies", order_by="started DESC") + return await get( + conn, Progress, type="import-imdb-movies", order_by=(progress.c.started, "desc") + ) -async def stop_import_progress(*, error: BaseException | None = None): +async def stop_import_progress( + conn: Connection, /, *, error: BaseException | None = None +) -> None: """Stop the current import. If an error is given, it will be logged to the progress state. """ - current = await get_import_progress() + current = await get_import_progress(conn) is_running = current and current.stopped is None if not is_running: @@ -151,17 +167,17 @@ async def stop_import_progress(*, error: BaseException | None = None): current.error = repr(error) current.stopped = utcnow().isoformat() - await update(current) + await update(conn, current) -async def set_import_progress(progress: float) -> Progress: +async def set_import_progress(conn: Connection, /, progress: float) -> Progress: """Set the current import progress percentage. If no import is currently running, this will create a new one. """ progress = min(max(0.0, progress), 100.0) # clamp to 0 <= progress <= 100 - current = await get_import_progress() + current = await get_import_progress(conn) is_running = current and current.stopped is None if not is_running: @@ -171,163 +187,211 @@ async def set_import_progress(progress: float) -> Progress: current.percent = progress if is_running: - await update(current) + await update(conn, current) else: - await add(current) + await add(conn, current) return current -_lock = threading.Lock() -_prelock = threading.Lock() +def _new_engine() -> AsyncEngine: + uri = f"sqlite+aiosqlite:///{config.storage_path}" + + return create_async_engine( + uri, + isolation_level="SERIALIZABLE", + ) + + +def _shared_engine() -> AsyncEngine: + global _engine + + if _engine is None: + _engine = _new_engine() + + return _engine + + +def _new_connection() -> Connection: + return _shared_engine().connect() @contextlib.asynccontextmanager -async def single_threaded(): - """Ensure the nested code is run only by a single thread at a time.""" - wait = 1e-5 # XXX not sure if there's a better magic value here +async def transaction( + *, force_rollback: bool = False +) -> AsyncGenerator[Connection, None]: + async with new_connection() as conn: + yield conn - # The pre-lock (a lock for the lock) allows for multiple threads to hand of - # the main lock. - # With only a single lock the contending thread will spend most of its time - # in the asyncio.sleep and the reigning thread will have time to finish - # whatever it's doing and simply acquire the lock again before the other - # thread has had a change to try. - # By having another lock (and the same sleep time!) the contending thread - # will always have a chance to acquire the main lock. - while not _prelock.acquire(blocking=False): - await asyncio.sleep(wait) + if not force_rollback: + await conn.commit() - try: - while not _lock.acquire(blocking=False): - await asyncio.sleep(wait) - finally: - _prelock.release() - try: - yield - - finally: - _lock.release() +# The _test_connection allows pinning a connection that will be shared across the app. +# This can (and should only) be used when running tests, NOT IN PRODUCTION! +_test_connection: Connection | None = None @contextlib.asynccontextmanager -async def locked_connection(): - async with single_threaded(): - yield shared_connection() +async def new_connection() -> AsyncGenerator[Connection, None]: + """Return a new connection. + + Any changes will be rolled back, unless `.commit()` is called on the + connection. + + If you want to commit changes, consider using `transaction()` instead. + """ + conn = _test_connection or _new_connection() + + # Support reusing the same connection for _test_connection. + is_started = conn.sync_connection is not None + if is_started: + yield conn + return + + async with conn: + yield conn -def shared_connection() -> Database: - global _shared_connection +@contextlib.asynccontextmanager +async def transacted( + conn: Connection, /, *, force_rollback: bool = False +) -> AsyncGenerator[None, None]: + transaction = contextlib.nullcontext() if conn.in_transaction() else conn.begin() - if _shared_connection is None: - uri = f"sqlite:///{config.storage_path}" - _shared_connection = Database(uri) + async with transaction: + try: + yield - return _shared_connection + finally: + if force_rollback: + await conn.rollback() -async def add(item): +async def add(conn: Connection, /, item: Model) -> None: # Support late initializing - used for optimization. if getattr(item, "_is_lazy", False): - item._lazy_init() + assert hasattr(item, "_lazy_init") + item._lazy_init() # pyright: ignore [reportGeneralTypeIssues] + table: sa.Table = item.__table__ values = asplain(item, serialize=True) - keys = ", ".join(f"{k}" for k in values) - placeholders = ", ".join(f":{k}" for k in values) - query = f"INSERT INTO {item._table} ({keys}) VALUES ({placeholders})" - async with locked_connection() as conn: - await conn.execute(query=query, values=values) + stmt = table.insert().values(values) + await conn.execute(stmt) -ModelType = TypeVar("ModelType") +async def fetch_all( + conn: Connection, /, query: sa.Executable, values: "dict | None" = None +) -> Sequence[sa.Row]: + result = await conn.execute(query, values) + return result.all() + + +async def fetch_one( + conn: Connection, /, query: sa.Executable, values: "dict | None" = None +) -> sa.Row | None: + result = await conn.execute(query, values) + return result.first() + + +ModelType = TypeVar("ModelType", bound=Model) async def get( - model: Type[ModelType], *, order_by: str | None = None, **kwds + conn: Connection, + /, + model: Type[ModelType], + *, + order_by: tuple[sa.Column, Literal["asc", "desc"]] | None = None, + **field_values, ) -> ModelType | None: """Load a model instance from the database. - Passing `kwds` allows to filter the instance to load. You have to encode the + Passing `field_values` allows to filter the item to load. You have to encode the values as the appropriate data type for the database prior to passing them to this function. """ - values = {k: v for k, v in kwds.items() if v is not None} - if not values: + if not field_values: return - fields_ = ", ".join(f.name for f in fields(model)) - cond = " AND ".join(f"{k}=:{k}" for k in values) - query = f"SELECT {fields_} FROM {model._table} WHERE {cond}" + table: sa.Table = model.__table__ + query = sa.select(model).where( + *(table.c[k] == v for k, v in field_values.items() if v is not None) + ) if order_by: - query += f" ORDER BY {order_by}" - async with locked_connection() as conn: - row = await conn.fetch_one(query=query, values=values) + order_col, order_dir = order_by + query = query.order_by( + order_col.asc() if order_dir == "asc" else order_col.desc() + ) + row = await fetch_one(conn, query) return fromplain(model, row._mapping, serialized=True) if row else None -async def get_many(model: Type[ModelType], **kwds) -> Iterable[ModelType]: - keys = { - k: [f"{k}_{i}" for i, _ in enumerate(vs, start=1)] for k, vs in kwds.items() - } +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. - if not keys: + This is similar to `get_all`, but instead of a scalar value a list of values + must be given. If any of the given values is set for that field on an item, + the item is considered a match. + If no field values are given, no items will be returned. + """ + if not field_sets: return [] - values = {n: v for k, vs in kwds.items() for n, v in zip(keys[k], vs)} + table: sa.Table = model.__table__ + query = sa.select(model).where(*(table.c[k].in_(v) for k, v in field_sets.items())) + rows = await fetch_all(conn, query) + return (fromplain(model, row._mapping, serialized=True) for row in rows) - fields_ = ", ".join(f.name for f in fields(model)) - cond = " AND ".join( - f"{k} IN ({','.join(':'+n for n in ns)})" for k, ns in keys.items() + +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. + """ + table: sa.Table = model.__table__ + query = sa.select(model).where( + *(table.c[k] == v for k, v in field_values.items() if v is not None) ) - query = f"SELECT {fields_} FROM {model._table} WHERE {cond}" - async with locked_connection() as conn: - rows = await conn.fetch_all(query=query, values=values) + rows = await fetch_all(conn, query) return (fromplain(model, row._mapping, serialized=True) for row in rows) -async def get_all(model: Type[ModelType], **kwds) -> Iterable[ModelType]: - values = {k: v for k, v in kwds.items() if v is not None} - - fields_ = ", ".join(f.name for f in fields(model)) - cond = " AND ".join(f"{k}=:{k}" for k in values) or "1=1" - query = f"SELECT {fields_} FROM {model._table} WHERE {cond}" - async with locked_connection() as conn: - rows = await conn.fetch_all(query=query, values=values) - return (fromplain(model, row._mapping, serialized=True) for row in rows) - - -async def update(item): +async def update(conn: Connection, /, item: Model) -> None: # Support late initializing - used for optimization. if getattr(item, "_is_lazy", False): - item._lazy_init() + assert hasattr(item, "_lazy_init") + item._lazy_init() # pyright: ignore [reportGeneralTypeIssues] + table: sa.Table = item.__table__ values = asplain(item, serialize=True) - keys = ", ".join(f"{k}=:{k}" for k in values if k != "id") - query = f"UPDATE {item._table} SET {keys} WHERE id=:id" - async with locked_connection() as conn: - await conn.execute(query=query, values=values) + stmt = table.update().where(table.c.id == values["id"]).values(values) + await conn.execute(stmt) -async def remove(item): +async def remove(conn: Connection, /, item: Model) -> None: + table: sa.Table = item.__table__ values = asplain(item, filter_fields={"id"}, serialize=True) - query = f"DELETE FROM {item._table} WHERE id=:id" - async with locked_connection() as conn: - await conn.execute(query=query, values=values) + stmt = table.delete().where(table.c.id == values["id"]) + await conn.execute(stmt) -async def add_or_update_user(user: User): - db_user = await get(User, imdb_id=user.imdb_id) +async def add_or_update_user(conn: Connection, /, user: User) -> None: + db_user = await get(conn, User, imdb_id=user.imdb_id) if not db_user: - await add(user) + await add(conn, user) else: user.id = db_user.id if user != db_user: - await update(user) + await update(conn, user) -async def add_or_update_many_movies(movies: list[Movie]): +async def add_or_update_many_movies(conn: Connection, /, movies: list[Movie]) -> None: """Add or update Movies in the database. This is an optimized version of `add_or_update_movie` for the purpose @@ -336,12 +400,13 @@ async def add_or_update_many_movies(movies: list[Movie]): # for movie in movies: # await add_or_update_movie(movie) db_movies = { - m.imdb_id: m for m in await get_many(Movie, imdb_id=[m.imdb_id for m in movies]) + m.imdb_id: m + for m in await get_many(conn, Movie, imdb_id=[m.imdb_id for m in movies]) } for movie in movies: # XXX optimize bulk add & update as well if movie.imdb_id not in db_movies: - await add(movie) + await add(conn, movie) else: db_movie = db_movies[movie.imdb_id] movie.id = db_movie.id @@ -354,10 +419,10 @@ async def add_or_update_many_movies(movies: list[Movie]): if movie.updated <= db_movie.updated: return - await update(movie) + await update(conn, movie) -async def add_or_update_movie(movie: Movie): +async def add_or_update_movie(conn: Connection, /, movie: Movie) -> None: """Add or update a Movie in the database. This is an upsert operation, but it will also update the Movie you pass @@ -365,9 +430,9 @@ async def add_or_update_movie(movie: Movie): set all optional values on your Movie that might be unset but exist in the database. It's a bidirectional sync. """ - db_movie = await get(Movie, imdb_id=movie.imdb_id) + db_movie = await get(conn, Movie, imdb_id=movie.imdb_id) if not db_movie: - await add(movie) + await add(conn, movie) else: movie.id = db_movie.id @@ -379,33 +444,35 @@ async def add_or_update_movie(movie: Movie): if movie.updated <= db_movie.updated: return - await update(movie) + await update(conn, movie) -async def add_or_update_rating(rating: Rating) -> bool: +async def add_or_update_rating(conn: Connection, /, rating: Rating) -> bool: db_rating = await get( - Rating, movie_id=str(rating.movie_id), user_id=str(rating.user_id) + conn, Rating, movie_id=str(rating.movie_id), user_id=str(rating.user_id) ) if not db_rating: - await add(rating) + await add(conn, rating) return True else: rating.id = db_rating.id if rating != db_rating: - await update(rating) + await update(conn, rating) return True return False -def sql_escape(s: str, char="#"): +def sql_escape(s: str, char: str = "#") -> str: return s.replace(char, 2 * char).replace("%", f"{char}%").replace("_", f"{char}_") async def find_ratings( + conn: Connection, + /, *, title: str | None = None, media_type: str | None = None, @@ -415,163 +482,129 @@ async def find_ratings( yearcomp: tuple[Literal["<", "=", ">"], int] | None = None, limit_rows: int = 10, user_ids: Iterable[str] = [], -): - values: dict[str, int | str] = { - "limit_rows": limit_rows, - } - +) -> Iterable[dict[str, Any]]: conditions = [] + if title: - values["escape"] = "#" - escaped_title = sql_escape(title, char=values["escape"]) - values["pattern"] = ( + escape_char = "#" + escaped_title = sql_escape(title, char=escape_char) + pattern = ( "_".join(escaped_title.split()) if exact else "%" + "%".join(escaped_title.split()) + "%" ) conditions.append( - f""" - ( - {Movie._table}.title LIKE :pattern ESCAPE :escape - OR {Movie._table}.original_title LIKE :pattern ESCAPE :escape + sa.or_( + movies.c.title.like(pattern, escape=escape_char), + movies.c.original_title.like(pattern, escape=escape_char), ) - """ ) - if yearcomp: - op, year = yearcomp - assert op in "<=>" - values["year"] = year - conditions.append(f"{Movie._table}.release_year{op}:year") + match yearcomp: + case ("<", year): + conditions.append(movies.c.release_year < year) + case ("=", year): + conditions.append(movies.c.release_year == year) + case (">", year): + conditions.append(movies.c.release_year > year) - if media_type: - values["media_type"] = media_type - conditions.append(f"{Movie._table}.media_type=:media_type") + if media_type is not None: + conditions.append(movies.c.media_type == media_type) if ignore_tv_episodes: - conditions.append(f"{Movie._table}.media_type!='TV Episode'") + conditions.append(movies.c.media_type != "TV Episode") - user_condition = "1=1" + user_condition = [] if user_ids: - uvs = {f"user_id_{i}": v for i, v in enumerate(user_ids, start=1)} - values.update(uvs) - user_condition = f"{Rating._table}.user_id IN ({','.join(':'+n for n in uvs)})" + user_condition.append(ratings.c.user_id.in_(user_ids)) - query = f""" - SELECT DISTINCT {Rating._table}.movie_id - FROM {Rating._table} - LEFT JOIN {Movie._table} ON {Movie._table}.id={Rating._table}.movie_id - WHERE {user_condition}{(' AND ' + ' AND '.join(conditions)) if conditions else ''} - ORDER BY length({Movie._table}.title) ASC, {Rating._table}.rating_date DESC, {Movie._table}.imdb_score DESC - LIMIT :limit_rows - """ - async with locked_connection() as conn: - rows = await conn.fetch_all(bindparams(query, values)) - movie_ids = tuple(r._mapping["movie_id"] for r in rows) + query = ( + sa.select(ratings.c.movie_id) + .distinct() + .outerjoin_from(ratings, movies, movies.c.id == ratings.c.movie_id) + .where(*conditions, *user_condition) + .order_by( + sa.func.length(movies.c.title).asc(), + ratings.c.rating_date.desc(), + movies.c.imdb_score.desc(), + ) + .limit(limit_rows) + ) + rating_rows: sa.CursorResult[Rating] = await conn.execute(query) + movie_ids = [r.movie_id for r in rating_rows] if include_unrated and len(movie_ids) < limit_rows: - sqlin, sqlin_vals = sql_in("id", movie_ids, not_=True) - query = f""" - SELECT DISTINCT id AS movie_id - FROM {Movie._table} - WHERE {sqlin} - {('AND ' + ' AND '.join(conditions)) if conditions else ''} - ORDER BY length(title) ASC, imdb_score DESC, release_year DESC - LIMIT :limit_rows - """ - async with locked_connection() as conn: - rows = await conn.fetch_all( - bindparams( - query, - {**values, **sqlin_vals, "limit_rows": limit_rows - len(movie_ids)}, - ) + query = ( + sa.select(movies.c.id) + .distinct() + .where(movies.c.id.not_in(movie_ids), *conditions) + .order_by( + sa.func.length(movies.c.title).asc(), + movies.c.imdb_score.desc(), + movies.c.release_year.desc(), ) - movie_ids += tuple(r._mapping["movie_id"] for r in rows) + .limit(limit_rows - len(movie_ids)) + ) + movie_rows: sa.CursorResult[Movie] = await conn.execute(query) + movie_ids += [r.id for r in movie_rows] - return await ratings_for_movie_ids(ids=movie_ids) + return await ratings_for_movie_ids(conn, ids=movie_ids) async def ratings_for_movie_ids( - ids: Iterable[ULID | str] = [], imdb_ids: Iterable[str] = [] + conn: Connection, + /, + ids: Iterable[ULID | str] = [], + imdb_ids: Iterable[str] = [], ) -> Iterable[dict[str, Any]]: - conds: list[str] = [] - vals: dict[str, str] = {} + conds = [] if ids: - sqlin, sqlin_vals = sql_in(f"{Movie._table}.id", (str(x) for x in ids)) - conds.append(sqlin) - vals.update(sqlin_vals) + conds.append(movies.c.id.in_([str(x) for x in ids])) if imdb_ids: - sqlin, sqlin_vals = sql_in(f"{Movie._table}.imdb_id", imdb_ids) - conds.append(sqlin) - vals.update(sqlin_vals) + conds.append(movies.c.imdb_id.in_(imdb_ids)) if not conds: return [] - query = f""" - SELECT - {Rating._table}.score AS user_score, - {Rating._table}.user_id AS user_id, - {Movie._table}.imdb_score, - {Movie._table}.imdb_votes, - {Movie._table}.imdb_id AS movie_imdb_id, - {Movie._table}.media_type AS media_type, - {Movie._table}.title AS canonical_title, - {Movie._table}.original_title AS original_title, - {Movie._table}.release_year AS release_year - FROM {Movie._table} - LEFT JOIN {Rating._table} ON {Movie._table}.id={Rating._table}.movie_id - WHERE {(' OR '.join(conds))} - """ - - async with locked_connection() as conn: - rows = await conn.fetch_all(bindparams(query, vals)) + query = ( + sa.select( + ratings.c.score.label("user_score"), + ratings.c.user_id.label("user_id"), + movies.c.imdb_score, + movies.c.imdb_votes, + movies.c.imdb_id.label("movie_imdb_id"), + movies.c.media_type.label("media_type"), + movies.c.title.label("canonical_title"), + movies.c.original_title.label("original_title"), + movies.c.release_year.label("release_year"), + ) + .outerjoin_from(movies, ratings, movies.c.id == ratings.c.movie_id) + .where(sa.or_(*conds)) + ) + rows = await fetch_all(conn, query) return tuple(dict(r._mapping) for r in rows) -def sql_fields(tp: Type): - return (f"{tp._table}.{f.name}" for f in fields(tp)) - - -def sql_in(column: str, values: Iterable[T], not_=False) -> tuple[str, dict[str, T]]: - c = column.replace(".", "___") - value_map = {f"{c}_{i}": v for i, v in enumerate(values, start=1)} - placeholders = ",".join(":" + k for k in value_map) - if not_: - return f"{column} NOT IN ({placeholders})", value_map - return f"{column} IN ({placeholders})", value_map - - async def ratings_for_movies( - movie_ids: Iterable[ULID], user_ids: Iterable[ULID] = [] + conn: Connection, /, movie_ids: Iterable[ULID], user_ids: Iterable[ULID] = [] ) -> Iterable[Rating]: - values: dict[str, str] = {} - conditions: list[str] = [] - - q, vm = sql_in("movie_id", [str(m) for m in movie_ids]) - conditions.append(q) - values.update(vm) + conditions = [ratings.c.movie_id.in_(str(x) for x in movie_ids)] if user_ids: - q, vm = sql_in("user_id", [str(m) for m in user_ids]) - conditions.append(q) - values.update(vm) + conditions.append(ratings.c.user_id.in_(str(x) for x in user_ids)) - query = f""" - SELECT {','.join(sql_fields(Rating))} - FROM {Rating._table} - WHERE {' AND '.join(f'({c})' for c in conditions) if conditions else '1=1'} - """ + query = sa.select(ratings).where(*conditions) - async with locked_connection() as conn: - rows = await conn.fetch_all(query, values) + rows = await fetch_all(conn, query) return (fromplain(Rating, row._mapping, serialized=True) for row in rows) async def find_movies( + conn: Connection, + /, *, title: str | None = None, media_type: str | None = None, @@ -583,88 +616,63 @@ async def find_movies( include_unrated: bool = False, user_ids: list[ULID] = [], ) -> Iterable[tuple[Movie, list[Rating]]]: - values: dict[str, int | str] = { - "limit_rows": limit_rows, - "skip_rows": skip_rows, - } - conditions = [] + if title: - values["escape"] = "#" - escaped_title = sql_escape(title, char=values["escape"]) - values["pattern"] = ( + escape_char = "#" + escaped_title = sql_escape(title, char=escape_char) + pattern = ( "_".join(escaped_title.split()) if exact else "%" + "%".join(escaped_title.split()) + "%" ) conditions.append( - f""" - ( - {Movie._table}.title LIKE :pattern ESCAPE :escape - OR {Movie._table}.original_title LIKE :pattern ESCAPE :escape + sa.or_( + movies.c.title.like(pattern, escape=escape_char), + movies.c.original_title.like(pattern, escape=escape_char), ) - """ ) - if yearcomp: - op, year = yearcomp - assert op in "<=>" - values["year"] = year - conditions.append(f"{Movie._table}.release_year{op}:year") + match yearcomp: + case ("<", year): + conditions.append(movies.c.release_year < year) + case ("=", year): + conditions.append(movies.c.release_year == year) + case (">", year): + conditions.append(movies.c.release_year > year) - if media_type: - values["media_type"] = media_type - conditions.append(f"{Movie._table}.media_type=:media_type") + if media_type is not None: + conditions.append(movies.c.media_type == media_type) if ignore_tv_episodes: - conditions.append(f"{Movie._table}.media_type!='TV Episode'") + conditions.append(movies.c.media_type != "TV Episode") if not include_unrated: - conditions.append(f"{Movie._table}.imdb_score NOTNULL") + conditions.append(movies.c.imdb_score.is_not(None)) - query = f""" - SELECT {','.join(sql_fields(Movie))} - FROM {Movie._table} - WHERE {(' AND '.join(conditions)) if conditions else '1=1'} - ORDER BY - length({Movie._table}.title) ASC, - {Movie._table}.imdb_score DESC, - {Movie._table}.release_year DESC - LIMIT :skip_rows, :limit_rows - """ - async with locked_connection() as conn: - rows = await conn.fetch_all(bindparams(query, values)) + query = ( + sa.select(movies) + .where(*conditions) + .order_by( + sa.func.length(movies.c.title).asc(), + movies.c.imdb_score.desc(), + movies.c.release_year.desc(), + ) + .limit(limit_rows) + .offset(skip_rows) + ) - movies = [fromplain(Movie, row._mapping, serialized=True) for row in rows] + rows = await fetch_all(conn, query) + + movies_ = [fromplain(Movie, row._mapping, serialized=True) for row in rows] if not user_ids: - return ((m, []) for m in movies) + return ((m, []) for m in movies_) - ratings = await ratings_for_movies((m.id for m in movies), user_ids) + ratings = await ratings_for_movies(conn, (m.id for m in movies_), user_ids) - aggreg: dict[ULID, tuple[Movie, list[Rating]]] = {m.id: (m, []) for m in movies} + aggreg: dict[ULID, tuple[Movie, list[Rating]]] = {m.id: (m, []) for m in movies_} for rating in ratings: aggreg[rating.movie_id][1].append(rating) return aggreg.values() - - -def bindparams(query: str, values: dict): - """Bind values to a query. - - This is similar to what SQLAlchemy and Databases do, but it allows to - easily use the same placeholder in multiple places. - """ - pump_vals = {} - pump_keys = {} - - def pump(match): - key = match[1] - val = values[key] - pump_keys[key] = 1 + pump_keys.setdefault(key, 0) - pump_key = f"{key}_{pump_keys[key]}" - pump_vals[pump_key] = val - return f":{pump_key}" - - pump_query = re.sub(r":(\w+)\b", pump, query) - return sqlalchemy.text(pump_query).bindparams(**pump_vals) diff --git a/unwind/imdb.py b/unwind/imdb.py index 477ec64..631a088 100644 --- a/unwind/imdb.py +++ b/unwind/imdb.py @@ -4,6 +4,8 @@ from collections import namedtuple from datetime import datetime from urllib.parse import urljoin +import bs4 + from . import db from .models import Movie, Rating, User from .request import asession, asoup_from_url, cache_path @@ -38,12 +40,14 @@ async def refresh_user_ratings_from_imdb(stop_on_dupe: bool = True): async with asession() as s: s.headers["Accept-Language"] = "en-US, en;q=0.5" - for user in await db.get_all(User): + async with db.new_connection() as conn: + users = list(await db.get_all(conn, User)) + for user in users: log.info("⚡️ Loading data for %s ...", user.name) try: async for rating, is_updated in load_ratings(user.imdb_id): - assert rating.user.id == user.id + assert rating.user is not None and rating.user.id == user.id if stop_on_dupe and not is_updated: break @@ -94,7 +98,7 @@ find_year = re.compile( find_movie_id = re.compile(r"/title/(?Ptt\d+)/").search -def movie_and_rating_from_item(item) -> tuple[Movie, Rating]: +def movie_and_rating_from_item(item: bs4.Tag) -> tuple[Movie, Rating]: genres = (genre := item.find("span", "genre")) and genre.string or "" movie = Movie( title=item.h3.a.string.strip(), @@ -154,13 +158,19 @@ async def parse_page(url: str) -> tuple[list[Rating], str | None]: soup = await asoup_from_url(url) - meta = soup.find("meta", property="pageId") - headline = soup.h1 - assert meta is not None and headline is not None + if (meta := soup.find("meta", property="pageId")) is None: + raise RuntimeError("No pageId found.") + assert isinstance(meta, bs4.Tag) imdb_id = meta["content"] - user = await db.get(User, imdb_id=imdb_id) or User( - imdb_id=imdb_id, name="", secret="" - ) + assert isinstance(imdb_id, str) + async with db.new_connection() as conn: + user = await db.get(conn, User, imdb_id=imdb_id) or User( + imdb_id=imdb_id, name="", secret="" + ) + + if (headline := soup.h1) is None: + raise RuntimeError("No headline found.") + assert isinstance(headline.string, str) if match := find_name(headline.string): user.name = match["name"] @@ -184,9 +194,15 @@ async def parse_page(url: str) -> tuple[list[Rating], str | None]: ratings.append(rating) - footer = soup.find("div", "footer") - assert footer is not None - next_url = urljoin(url, footer.find(string=re.compile(r"Next")).parent["href"]) + next_url = None + if (footer := soup.find("div", "footer")) is None: + raise RuntimeError("No footer found.") + assert isinstance(footer, bs4.Tag) + if (next_link := footer.find("a", string="Next")) is not None: + assert isinstance(next_link, bs4.Tag) + next_href = next_link["href"] + assert isinstance(next_href, str) + next_url = urljoin(url, next_href) return (ratings, next_url if url != next_url else None) @@ -200,14 +216,15 @@ async def load_ratings(user_id: str): for i, rating in enumerate(ratings): assert rating.user and rating.movie - if i == 0: - # All rating objects share the same user. - await db.add_or_update_user(rating.user) - rating.user_id = rating.user.id + async with db.transaction() as conn: + if i == 0: + # All rating objects share the same user. + await db.add_or_update_user(conn, rating.user) + rating.user_id = rating.user.id - await db.add_or_update_movie(rating.movie) - rating.movie_id = rating.movie.id + await db.add_or_update_movie(conn, rating.movie) + rating.movie_id = rating.movie.id - is_updated = await db.add_or_update_rating(rating) + is_updated = await db.add_or_update_rating(conn, rating) yield rating, is_updated diff --git a/unwind/imdb_import.py b/unwind/imdb_import.py index 705db2f..dad419e 100644 --- a/unwind/imdb_import.py +++ b/unwind/imdb_import.py @@ -209,7 +209,8 @@ async def import_from_file(*, basics_path: Path, ratings_path: Path): for i, m in enumerate(read_basics(basics_path)): perc = 100 * i / total if perc >= perc_next_report: - await db.set_import_progress(perc) + async with db.transaction() as conn: + await db.set_import_progress(conn, perc) log.info("⏳ Imported %s%%", round(perc, 1)) perc_next_report += perc_step @@ -233,15 +234,18 @@ async def import_from_file(*, basics_path: Path, ratings_path: Path): chunk.append(m) if len(chunk) > 1000: - await add_or_update_many_movies(chunk) + async with db.transaction() as conn: + await add_or_update_many_movies(conn, chunk) chunk = [] if chunk: - await add_or_update_many_movies(chunk) + async with db.transaction() as conn: + await add_or_update_many_movies(conn, chunk) chunk = [] log.info("👍 Imported 100%") - await db.set_import_progress(100) + async with db.transaction() as conn: + await db.set_import_progress(conn, 100) async def download_datasets(*, basics_path: Path, ratings_path: Path) -> None: @@ -270,7 +274,8 @@ async def load_from_web(*, force: bool = False) -> None: See https://www.imdb.com/interfaces/ and https://datasets.imdbws.com/ for more information on the IMDb database dumps. """ - await db.set_import_progress(0) + async with db.transaction() as conn: + await db.set_import_progress(conn, 0) try: ratings_file = config.datadir / "imdb/title.ratings.tsv.gz" @@ -290,8 +295,10 @@ async def load_from_web(*, force: bool = False) -> None: await import_from_file(basics_path=basics_file, ratings_path=ratings_file) except BaseException as err: - await db.stop_import_progress(error=err) + async with db.transaction() as conn: + await db.stop_import_progress(conn, error=err) raise else: - await db.stop_import_progress() + async with db.transaction() as conn: + await db.stop_import_progress(conn) diff --git a/unwind/models.py b/unwind/models.py index 4480307..3e52225 100644 --- a/unwind/models.py +++ b/unwind/models.py @@ -11,13 +11,18 @@ from typing import ( Container, Literal, Mapping, + Protocol, Type, + TypedDict, TypeVar, Union, get_args, get_origin, ) +from sqlalchemy import Column, ForeignKey, Integer, String, Table +from sqlalchemy.orm import registry + from .types import ULID JSON = int | float | str | None | list["JSON"] | dict[str, "JSON"] @@ -26,8 +31,16 @@ JSONObject = dict[str, JSON] T = TypeVar("T") +class Model(Protocol): + __table__: ClassVar[Table] + + +mapper_registry = registry() +metadata = mapper_registry.metadata + + def annotations(tp: Type) -> tuple | None: - return tp.__metadata__ if hasattr(tp, "__metadata__") else None + return tp.__metadata__ if hasattr(tp, "__metadata__") else None # type: ignore def fields(class_or_instance): @@ -112,7 +125,7 @@ def asplain( if filter_fields is not None and f.name not in filter_fields: continue - target = 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 @@ -156,7 +169,7 @@ def fromplain(cls: Type[T], d: Mapping, *, serialized: bool = False) -> T: dd: JSONObject = {} for f in fields(cls): - target = f.type + target: Any = f.type otype = optional_type(f.type) is_opt = otype is not None if is_opt: @@ -194,12 +207,38 @@ def validate(o: object) -> None: def utcnow(): - return datetime.utcnow().replace(tzinfo=timezone.utc) + 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: - _table: ClassVar[str] = "progress" + __table__: ClassVar[Table] = Table( + "progress", + metadata, + Column("id", String, primary_key=True), # ULID + Column("type", String, nullable=False), + Column("state", String, nullable=False), # JSON {"percent": ..., "error": ...} + Column("started", String, nullable=False), # datetime + Column("stopped", String), + ) id: ULID = field(default_factory=ULID) type: str = None @@ -236,9 +275,28 @@ class Progress: self._state = state +progress = Progress.__table__ + + +@mapper_registry.mapped @dataclass class Movie: - _table: ClassVar[str] = "movies" + __table__: ClassVar[Table] = Table( + "movies", + metadata, + Column("id", String, primary_key=True), # ULID + Column("title", String, nullable=False), + Column("original_title", String), + Column("release_year", Integer, nullable=False), + Column("media_type", String, nullable=False), + Column("imdb_id", String, nullable=False, unique=True), + Column("imdb_score", Integer), + Column("imdb_votes", Integer), + Column("runtime", Integer), + Column("genres", String, nullable=False), + Column("created", String, nullable=False), # datetime + Column("updated", String, nullable=False), # datetime + ) id: ULID = field(default_factory=ULID) title: str = None # canonical title (usually English) @@ -283,6 +341,8 @@ class Movie: self._is_lazy = False +movies = Movie.__table__ + _RelationSentinel = object() """Mark a model field as containing external data. @@ -294,9 +354,65 @@ The contents of the Relation are ignored or discarded when using Relation = Annotated[T | None, _RelationSentinel] +Access = Literal[ + "r", # read + "i", # index + "w", # write +] + + +class UserGroup(TypedDict): + id: str + access: Access + + +@mapper_registry.mapped +@dataclass +class User: + __table__: ClassVar[Table] = Table( + "users", + metadata, + Column("id", String, primary_key=True), # ULID + Column("imdb_id", String, nullable=False, unique=True), + Column("name", String, nullable=False), + Column("secret", String, nullable=False), + Column("groups", String, nullable=False), # JSON array + ) + + 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) + + def has_access(self, group_id: ULID | str, access: Access = "r"): + group_id = group_id if isinstance(group_id, str) else str(group_id) + return any(g["id"] == group_id and access == g["access"] for g in self.groups) + + def set_access(self, group_id: ULID | str, access: Access): + group_id = group_id if isinstance(group_id, str) else str(group_id) + for g in self.groups: + if g["id"] == group_id: + g["access"] = access + break + else: + self.groups.append({"id": group_id, "access": access}) + + +@mapper_registry.mapped @dataclass class Rating: - _table: ClassVar[str] = "ratings" + __table__: ClassVar[Table] = Table( + "ratings", + 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("score", Integer, nullable=False), + Column("rating_date", String, nullable=False), # datetime + Column("favorite", Integer), # bool + Column("finished", Integer), # bool + ) id: ULID = field(default_factory=ULID) @@ -304,7 +420,7 @@ class Rating: movie: Relation[Movie] = None user_id: ULID = None - user: Relation["User"] = None + user: Relation[User] = None score: int = None # range: [0,100] rating_date: datetime = None @@ -324,41 +440,25 @@ class Rating: ) -Access = Literal[ - "r", # read - "i", # index - "w", # write -] +ratings = Rating.__table__ -@dataclass -class User: - _table: ClassVar[str] = "users" - - id: ULID = field(default_factory=ULID) - imdb_id: str = None - name: str = None # canonical user name - secret: str = None - groups: list[dict[str, str]] = field(default_factory=list) - - def has_access(self, group_id: ULID | str, access: Access = "r"): - group_id = group_id if isinstance(group_id, str) else str(group_id) - return any(g["id"] == group_id and access == g["access"] for g in self.groups) - - def set_access(self, group_id: ULID | str, access: Access): - group_id = group_id if isinstance(group_id, str) else str(group_id) - for g in self.groups: - if g["id"] == group_id: - g["access"] = access - break - else: - self.groups.append({"id": group_id, "access": access}) +class GroupUser(TypedDict): + id: str + name: str +@mapper_registry.mapped @dataclass class Group: - _table: ClassVar[str] = "groups" + __table__: ClassVar[Table] = Table( + "groups", + metadata, + Column("id", String, primary_key=True), # ULID + Column("name", String, nullable=False), + Column("users", String, nullable=False), # JSON array + ) id: ULID = field(default_factory=ULID) name: str = None - users: list[dict[str, str]] = field(default_factory=list) + users: list[GroupUser] = field(default_factory=list) diff --git a/unwind/request.py b/unwind/request.py index 4e57564..b4a41d4 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 Callable, ParamSpec, TypeVar, cast +from typing import Any, Callable, ParamSpec, TypeVar, cast import bs4 import httpx @@ -190,9 +190,11 @@ async def asoup_from_url(url): def _last_modified_from_response(resp: _Response_T) -> float | None: if last_mod := resp.headers.get("last-modified"): try: - return email.utils.parsedate_to_datetime(last_mod).timestamp() - except: + dt = email.utils.parsedate_to_datetime(last_mod) + except ValueError: log.exception("🐛 Received invalid value for Last-Modified: %s", last_mod) + else: + return dt.timestamp() def _last_modified_from_file(path: Path) -> float: @@ -206,8 +208,8 @@ async def adownload( replace_existing: bool | None = None, only_if_newer: bool = False, timeout: float | None = None, - chunk_callback=None, - response_callback=None, + chunk_callback: Callable[[bytes], Any] | None = None, + response_callback: Callable[[_Response_T], Any] | None = None, ) -> bytes | None: """Download a file. @@ -246,7 +248,7 @@ async def adownload( if response_callback is not None: try: response_callback(resp) - except: + except BaseException: log.exception("🐛 Error in response callback.") log.debug( @@ -267,7 +269,9 @@ async def adownload( resp.raise_for_status() if to_path is None: - await resp.aread() # Download the response stream to allow `resp.content` access. + await ( + resp.aread() + ) # Download the response stream to allow `resp.content` access. return resp.content resp_lastmod = _last_modified_from_response(resp) @@ -275,7 +279,7 @@ async def adownload( # Check Last-Modified in case the server ignored If-Modified-Since. # XXX also check Content-Length? if file_exists and only_if_newer and resp_lastmod is not None: - assert file_lastmod + assert file_lastmod # pyright: ignore [reportUnboundVariable] if resp_lastmod <= file_lastmod: log.debug("✋ Local file is newer, skipping download: %a", req.url) @@ -299,7 +303,7 @@ async def adownload( if chunk_callback: try: chunk_callback(chunk) - except: + except BaseException: log.exception("🐛 Error in chunk callback.") finally: os.close(tempfd) diff --git a/unwind/web.py b/unwind/web.py index eb08e9c..8b02863 100644 --- a/unwind/web.py +++ b/unwind/web.py @@ -168,7 +168,8 @@ async def auth_user(request) -> User | None: if not isinstance(request.user, AuthedUser): return - user = await db.get(User, id=request.user.user_id) + async with db.new_connection() as conn: + user = await db.get(conn, User, id=request.user.user_id) if not user: return @@ -179,7 +180,7 @@ async def auth_user(request) -> User | None: return user -_routes = [] +_routes: list[Route] = [] def route(path: str, *, methods: list[str] | None = None, **kwds): @@ -191,16 +192,13 @@ def route(path: str, *, methods: list[str] | None = None, **kwds): return decorator -route.registered = _routes - - @route("/groups/{group_id}/ratings") async def get_ratings_for_group(request): group_id = as_ulid(request.path_params["group_id"]) - group = await db.get(Group, id=str(group_id)) - if not group: - return not_found() + async with db.new_connection() as conn: + if (group := await db.get(conn, Group, id=str(group_id))) is None: + return not_found() user_ids = {u["id"] for u in group.users} @@ -211,22 +209,26 @@ async def get_ratings_for_group(request): # if (imdb_id or unwind_id) and (movie := await db.get(Movie, id=unwind_id, imdb_id=imdb_id)): if unwind_id: - rows = await db.ratings_for_movie_ids(ids=[unwind_id]) + async with db.new_connection() as conn: + rows = await db.ratings_for_movie_ids(conn, ids=[unwind_id]) elif imdb_id: - rows = await db.ratings_for_movie_ids(imdb_ids=[imdb_id]) + async with db.new_connection() as conn: + rows = await db.ratings_for_movie_ids(conn, imdb_ids=[imdb_id]) else: - rows = await find_ratings( - title=params.get("title"), - media_type=params.get("media_type"), - exact=truthy(params.get("exact")), - ignore_tv_episodes=truthy(params.get("ignore_tv_episodes")), - include_unrated=truthy(params.get("include_unrated")), - yearcomp=yearcomp(params["year"]) if "year" in params else None, - limit_rows=as_int(params.get("per_page"), max=10, default=5), - user_ids=user_ids, - ) + async with db.new_connection() as conn: + rows = await find_ratings( + conn, + title=params.get("title"), + media_type=params.get("media_type"), + exact=truthy(params.get("exact")), + ignore_tv_episodes=truthy(params.get("ignore_tv_episodes")), + include_unrated=truthy(params.get("include_unrated")), + yearcomp=yearcomp(params["year"]) if "year" in params else None, + limit_rows=as_int(params.get("per_page"), max=10, default=5), + user_ids=user_ids, + ) ratings = (web_models.Rating(**r) for r in rows) @@ -265,7 +267,8 @@ async def list_movies(request): if group_id := params.get("group_id"): group_id = as_ulid(group_id) - group = await db.get(Group, id=str(group_id)) + async with db.new_connection() as conn: + group = await db.get(conn, Group, id=str(group_id)) if not group: return not_found("Group not found.") @@ -290,26 +293,31 @@ async def list_movies(request): if imdb_id or unwind_id: # XXX missing support for user_ids and user_scores - movies = ( - [m] if (m := await db.get(Movie, id=unwind_id, imdb_id=imdb_id)) else [] - ) + async with db.new_connection() as conn: + movies = ( + [m] + if (m := await db.get(conn, Movie, id=unwind_id, imdb_id=imdb_id)) + else [] + ) resp = [asplain(web_models.RatingAggregate.from_movie(m)) for m in movies] else: per_page = as_int(params.get("per_page"), max=1000, default=5) page = as_int(params.get("page"), min=1, default=1) - movieratings = await find_movies( - title=params.get("title"), - media_type=params.get("media_type"), - exact=truthy(params.get("exact")), - ignore_tv_episodes=truthy(params.get("ignore_tv_episodes")), - include_unrated=truthy(params.get("include_unrated")), - yearcomp=yearcomp(params["year"]) if "year" in params else None, - limit_rows=per_page, - skip_rows=(page - 1) * per_page, - user_ids=list(user_ids), - ) + async with db.new_connection() as conn: + movieratings = await find_movies( + conn, + title=params.get("title"), + media_type=params.get("media_type"), + exact=truthy(params.get("exact")), + ignore_tv_episodes=truthy(params.get("ignore_tv_episodes")), + include_unrated=truthy(params.get("include_unrated")), + yearcomp=yearcomp(params["year"]) if "year" in params else None, + limit_rows=per_page, + skip_rows=(page - 1) * per_page, + user_ids=list(user_ids), + ) resp = [] for movie, ratings in movieratings: @@ -329,7 +337,8 @@ async def add_movie(request): @route("/movies/_reload_imdb", methods=["GET"]) @requires(["authenticated", "admin"]) async def progress_for_load_imdb_movies(request): - progress = await db.get_import_progress() + async with db.new_connection() as conn: + progress = await db.get_import_progress(conn) if not progress: return JSONResponse({"status": "No import exists."}, status_code=404) @@ -368,14 +377,16 @@ async def load_imdb_movies(request): force = truthy(params.get("force")) async with _import_lock: - progress = await db.get_import_progress() + async with db.new_connection() as conn: + progress = await db.get_import_progress(conn) if progress and not progress.stopped: return JSONResponse( {"status": "Import is running.", "progress": progress.percent}, status_code=409, ) - await db.set_import_progress(0) + async with db.transaction() as conn: + await db.set_import_progress(conn, 0) task = BackgroundTask(imdb_import.load_from_web, force=force) return JSONResponse( @@ -386,7 +397,8 @@ async def load_imdb_movies(request): @route("/users") @requires(["authenticated", "admin"]) async def list_users(request): - users = await db.get_all(User) + async with db.new_connection() as conn: + users = await db.get_all(conn, User) return JSONResponse([asplain(u) for u in users]) @@ -402,7 +414,8 @@ async def add_user(request): secret = secrets.token_bytes() user = User(name=name, imdb_id=imdb_id, secret=phc_scrypt(secret)) - await db.add(user) + async with db.transaction() as conn: + await db.add(conn, user) return JSONResponse( { @@ -418,7 +431,8 @@ async def show_user(request): user_id = as_ulid(request.path_params["user_id"]) if is_admin(request): - user = await db.get(User, id=str(user_id)) + async with db.new_connection() as conn: + user = await db.get(conn, User, id=str(user_id)) else: user = await auth_user(request) @@ -445,14 +459,15 @@ async def show_user(request): async def remove_user(request): user_id = as_ulid(request.path_params["user_id"]) - user = await db.get(User, id=str(user_id)) + async with db.new_connection() as conn: + user = await db.get(conn, User, id=str(user_id)) if not user: return not_found() - async with db.shared_connection().transaction(): + async with db.transaction() as conn: # XXX remove user refs from groups and ratings - await db.remove(user) + await db.remove(conn, user) return JSONResponse(asplain(user)) @@ -463,7 +478,8 @@ async def modify_user(request): user_id = as_ulid(request.path_params["user_id"]) if is_admin(request): - user = await db.get(User, id=str(user_id)) + async with db.new_connection() as conn: + user = await db.get(conn, User, id=str(user_id)) else: user = await auth_user(request) @@ -499,7 +515,8 @@ async def modify_user(request): user.secret = phc_scrypt(secret) - await db.update(user) + async with db.transaction() as conn: + await db.update(conn, user) return JSONResponse(asplain(user)) @@ -509,13 +526,15 @@ async def modify_user(request): async def add_group_to_user(request): user_id = as_ulid(request.path_params["user_id"]) - user = await db.get(User, id=str(user_id)) + async with db.new_connection() as conn: + user = await db.get(conn, User, id=str(user_id)) if not user: return not_found("User not found") (group_id, access) = await json_from_body(request, ["group", "access"]) - group = await db.get(Group, id=str(group_id)) + async with db.new_connection() as conn: + group = await db.get(conn, Group, id=str(group_id)) if not group: return not_found("Group not found") @@ -523,7 +542,8 @@ async def add_group_to_user(request): raise HTTPException(422, f"Invalid access level.") user.set_access(group_id, access) - await db.update(user) + async with db.transaction() as conn: + await db.update(conn, user) return JSONResponse(asplain(user)) @@ -551,7 +571,8 @@ async def load_imdb_user_ratings(request): @route("/groups") @requires(["authenticated", "admin"]) async def list_groups(request): - groups = await db.get_all(Group) + async with db.new_connection() as conn: + groups = await db.get_all(conn, Group) return JSONResponse([asplain(g) for g in groups]) @@ -564,7 +585,8 @@ async def add_group(request): # XXX restrict name group = Group(name=name) - await db.add(group) + async with db.transaction() as conn: + await db.add(conn, group) return JSONResponse(asplain(group)) @@ -573,7 +595,8 @@ async def add_group(request): @requires(["authenticated"]) async def add_user_to_group(request): group_id = as_ulid(request.path_params["group_id"]) - group = await db.get(Group, id=str(group_id)) + async with db.new_connection() as conn: + group = await db.get(conn, Group, id=str(group_id)) if not group: return not_found() @@ -600,7 +623,8 @@ async def add_user_to_group(request): else: group.users.append({"name": name, "id": user_id}) - await db.update(group) + async with db.transaction() as conn: + await db.update(conn, group) return JSONResponse(asplain(group)) @@ -632,7 +656,7 @@ def create_app(): return Starlette( lifespan=lifespan, routes=[ - Mount(f"{config.api_base}v1", routes=route.registered), + Mount(f"{config.api_base}v1", routes=_routes), ], middleware=[ Middleware(ResponseTimeMiddleware, header_name="Unwind-Elapsed"),