From 01af0ead1d1508c17a26f8e20dca83fcbe5f57d2 Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:29:02 -0600 Subject: [PATCH 01/40] Update .gitignore --- .gitignore | 148 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 137 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 562885d..15bb465 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,10 @@ +# Misc. user-groups.dev.yaml user-groups.yaml orchestration.yaml my-archives *.pyc -.DS_Store secrets/* -*.log -__pycache__ -.pytest_cache -.env -.env.dev -.env.prod *.db redis/data/* .ipynb_checkpoints* @@ -18,8 +12,6 @@ app/user-groups.yaml app/user-groups.dev.yaml wit* app/crawls -.coverage -.pytest_cache/ htmlcov local_archive local_archive_test @@ -27,6 +19,140 @@ local_archive_test *db-shm copy-files.sh temp/ -.python-version orchestration2.yaml -database \ No newline at end of file +database + +# IDE files +.idea +.vscode +**/.DS_Store + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env* +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ From 7d04b69488ef8136e0de838e7acf8762599f80e8 Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:29:57 -0600 Subject: [PATCH 02/40] Create pull_request_template.md --- .github/pull_request_template.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..3ec80e9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,23 @@ + +## Describe your changes + + +## Non-obvious technical information + + +## Checklist before requesting a review + +- [ ] The code runs successfully. + +```commandline +HERE IS SOME COMMAND LINE OUTPUT +``` From 793e8db808eaba1e52b9ab12184bfeed544a5be4 Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:00:00 -0600 Subject: [PATCH 03/40] Update poetry.lock --- poetry.lock | 269 ++++++++++++++++++++++++++-------------------------- 1 file changed, 135 insertions(+), 134 deletions(-) diff --git a/poetry.lock b/poetry.lock index 79f7907..ceb824a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "aiosmtplib" @@ -53,7 +53,7 @@ SQLAlchemy = ">=1.3.0" typing-extensions = ">=4" [package.extras] -tz = ["backports.zoneinfo", "tzdata"] +tz = ["backports.zoneinfo ; python_version < \"3.9\"", "tzdata"] [[package]] name = "amqp" @@ -102,7 +102,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "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)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] [[package]] @@ -130,12 +130,12 @@ files = [ ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "authlib" @@ -250,18 +250,18 @@ files = [ [[package]] name = "boto3" -version = "1.36.21" +version = "1.36.26" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "boto3-1.36.21-py3-none-any.whl", hash = "sha256:f94faa7cf932d781f474d87f8b4c14a033af95ac1460136b40d75e7a30086ef0"}, - {file = "boto3-1.36.21.tar.gz", hash = "sha256:41eb2b73eb612d300e629e3328b83f1ffea0fc6633e75c241a72a76746c1db26"}, + {file = "boto3-1.36.26-py3-none-any.whl", hash = "sha256:f67d014a7c5a3cd540606d64d7cb9eec3600cf42acab1ac0518df9751ae115e2"}, + {file = "boto3-1.36.26.tar.gz", hash = "sha256:523b69457eee55ac15aa707c0e768b2a45ca1521f95b2442931090633ec72458"}, ] [package.dependencies] -botocore = ">=1.36.21,<1.37.0" +botocore = ">=1.36.26,<1.37.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.11.0,<0.12.0" @@ -270,14 +270,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.36.21" +version = "1.36.26" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "botocore-1.36.21-py3-none-any.whl", hash = "sha256:24a7052e792639dc2726001bd474cd0aaa959c1e18ddd92c17f3adc6efa1b132"}, - {file = "botocore-1.36.21.tar.gz", hash = "sha256:da746240e2ad64fd4997f7f3664a0a8e303d18075fc1d473727cb6375080ea16"}, + {file = "botocore-1.36.26-py3-none-any.whl", hash = "sha256:4e3f19913887a58502e71ef8d696fe7eaa54de7813ff73390cd5883f837dfa6e"}, + {file = "botocore-1.36.26.tar.gz", hash = "sha256:4a63bcef7ecf6146fd3a61dc4f9b33b7473b49bdaf1770e9aaca6eee0c9eab62"}, ] [package.dependencies] @@ -441,14 +441,14 @@ beautifulsoup4 = "*" [[package]] name = "cachetools" -version = "5.5.1" +version = "5.5.2" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"}, - {file = "cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95"}, + {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, + {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, ] [[package]] @@ -478,32 +478,32 @@ vine = ">=5.1.0,<6.0" arangodb = ["pyArango (>=2.0.2)"] auth = ["cryptography (==42.0.5)"] azureblockblob = ["azure-storage-blob (>=12.15.0)"] -brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] cassandra = ["cassandra-driver (>=3.25.0,<4)"] consul = ["python-consul2 (==0.1.5)"] cosmosdbsql = ["pydocumentdb (==2.3.5)"] -couchbase = ["couchbase (>=3.0.0)"] +couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] couchdb = ["pycouchdb (==1.14.2)"] django = ["Django (>=2.2.28)"] dynamodb = ["boto3 (>=1.26.143)"] elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] -eventlet = ["eventlet (>=0.32.0)"] +eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] gcs = ["google-cloud-storage (>=2.10.0)"] gevent = ["gevent (>=1.5.0)"] -librabbitmq = ["librabbitmq (>=2.0.0)"] -memcache = ["pylibmc (==1.6.3)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] mongodb = ["pymongo[srv] (>=4.0.2)"] msgpack = ["msgpack (==1.0.8)"] pymemcache = ["python-memcached (>=1.61)"] -pyro = ["pyro4 (==4.82)"] +pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] pytest = ["pytest-celery[all] (>=1.0.0)"] redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] s3 = ["boto3 (>=1.26.143)"] slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem (==4.1.5)"] +solar = ["ephem (==4.1.5) ; platform_python_implementation != \"PyPy\""] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] -tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] +tblib = ["tblib (>=1.3.0) ; python_version < \"3.8.0\"", "tblib (>=1.5.0) ; python_version >= \"3.8.0\""] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] zstd = ["zstandard (==0.22.0)"] @@ -872,7 +872,7 @@ files = [ ] [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" @@ -1130,7 +1130,7 @@ requests = ">=2.18.0,<3.0.0.dev0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -1215,14 +1215,14 @@ tool = ["click (>=6.0.0)"] [[package]] name = "googleapis-common-protos" -version = "1.67.0" +version = "1.68.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "googleapis_common_protos-1.67.0-py2.py3-none-any.whl", hash = "sha256:579de760800d13616f51cf8be00c876f00a9f146d3e6510e19d1f4111758b741"}, - {file = "googleapis_common_protos-1.67.0.tar.gz", hash = "sha256:21398025365f138be356d5923e9168737d94d46a72aefee4a6110a1f23463c86"}, + {file = "googleapis_common_protos-1.68.0-py2.py3-none-any.whl", hash = "sha256:aaf179b2f81df26dfadac95def3b16a95064c76a5f45f07e4c68a21bb371c4ac"}, + {file = "googleapis_common_protos-1.68.0.tar.gz", hash = "sha256:95d38161f4f9af0d9423eed8fb7b64ffd2568c3464eb542ff02c5bfa1953ab3c"}, ] [package.dependencies] @@ -1403,7 +1403,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -1521,7 +1521,7 @@ azureservicebus = ["azure-servicebus (>=7.10.0)"] azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] confluentkafka = ["confluent-kafka (>=2.2.0)"] consul = ["python-consul2 (==0.1.5)"] -librabbitmq = ["librabbitmq (>=2.0.0)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] mongodb = ["pymongo (>=4.1.1)"] msgpack = ["msgpack (==1.1.0)"] pyro = ["pyro4 (==4.82)"] @@ -1529,7 +1529,7 @@ qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] slmq = ["softlayer-messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] @@ -1550,7 +1550,7 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] -dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"] +dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] [[package]] name = "mako" @@ -1978,7 +1978,7 @@ docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline fpx = ["olefile"] mic = ["olefile"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] @@ -2109,7 +2109,7 @@ files = [ ] [package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] +test = ["enum34 ; python_version <= \"3.4\"", "ipaddress ; python_version < \"3.0\"", "mock ; python_version < \"3.0\"", "pywin32 ; sys_platform == \"win32\"", "wmi ; sys_platform == \"win32\""] [[package]] name = "pyaes" @@ -2222,7 +2222,7 @@ typing-extensions = ">=4.12.2" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -2339,14 +2339,14 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.7.1" +version = "2.8.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" groups = ["main", "web"] files = [ - {file = "pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"}, - {file = "pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93"}, + {file = "pydantic_settings-2.8.0-py3-none-any.whl", hash = "sha256:c782c7dc3fb40e97b238e713c25d26f64314aece2e91abcff592fcac15f71820"}, + {file = "pydantic_settings-2.8.0.tar.gz", hash = "sha256:88e2ca28f6e68ea102c99c3c401d6c9078e68a5df600e97b43891c34e089500a"}, ] [package.dependencies] @@ -2936,14 +2936,14 @@ crt = ["botocore[crt] (>=1.36.0,<2.0a.0)"] [[package]] name = "selenium" -version = "4.28.1" +version = "4.29.0" description = "Official Python bindings for Selenium WebDriver" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "selenium-4.28.1-py3-none-any.whl", hash = "sha256:4238847e45e24e4472cfcf3554427512c7aab9443396435b1623ef406fff1cc1"}, - {file = "selenium-4.28.1.tar.gz", hash = "sha256:0072d08670d7ec32db901bd0107695a330cecac9f196e3afb3fa8163026e022a"}, + {file = "selenium-4.29.0-py3-none-any.whl", hash = "sha256:ce5d26f1ddc1111641113653af33694c13947dd36c2df09cdd33f554351d372e"}, + {file = "selenium-4.29.0.tar.gz", hash = "sha256:3a62f7ec33e669364a6c0562a701deb69745b569c50d55f1a912bf8eb33358ba"}, ] [package.dependencies] @@ -2967,13 +2967,13 @@ files = [ ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] -core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -3139,14 +3139,14 @@ full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart [[package]] name = "telethon" -version = "1.38.1" +version = "1.39.0" description = "Full-featured Telegram client library for Python 3" optional = false python-versions = ">=3.5" groups = ["main"] files = [ - {file = "Telethon-1.38.1-py3-none-any.whl", hash = "sha256:30c187017501bfb982b8af5659f864dda4108f77ea49cfce61e8f6fdb8a18d6e"}, - {file = "Telethon-1.38.1.tar.gz", hash = "sha256:f9866c1e37197a0894e0c02aa56a6359bffb14a585e88e18e3e819df4fda399a"}, + {file = "Telethon-1.39.0-py3-none-any.whl", hash = "sha256:aa9f394b94be144799a6f6a93ab463867bc7c63503ede9631751940a98f6c703"}, + {file = "telethon-1.39.0.tar.gz", hash = "sha256:35d4795d8c91deac515fb0bcb3723866b924de1c724e1d5c230460e96f284a63"}, ] [package.dependencies] @@ -3256,18 +3256,19 @@ sortedcontainers = "*" [[package]] name = "trio-websocket" -version = "0.11.1" +version = "0.12.1" description = "WebSocket library for Trio" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, - {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, + {file = "trio_websocket-0.12.1-py3-none-any.whl", hash = "sha256:608ec746bb287e5d5a66baf483e41194193c5cf05ffaad6240e7d1fcd80d1e6f"}, + {file = "trio_websocket-0.12.1.tar.gz", hash = "sha256:d55ccd4d3eae27c494f3fdae14823317839bdcb8214d1173eacc4d42c69fc91b"}, ] [package.dependencies] exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +outcome = ">=1.2.0" trio = ">=0.11" wsproto = ">=0.14" @@ -3377,7 +3378,7 @@ files = [ pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -3400,7 +3401,7 @@ h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "vine" @@ -3554,81 +3555,81 @@ test = ["websockets"] [[package]] name = "websockets" -version = "14.2" +version = "15.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "websockets-14.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e8179f95323b9ab1c11723e5d91a89403903f7b001828161b480a7810b334885"}, - {file = "websockets-14.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d8c3e2cdb38f31d8bd7d9d28908005f6fa9def3324edb9bf336d7e4266fd397"}, - {file = "websockets-14.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:714a9b682deb4339d39ffa674f7b674230227d981a37d5d174a4a83e3978a610"}, - {file = "websockets-14.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2e53c72052f2596fb792a7acd9704cbc549bf70fcde8a99e899311455974ca3"}, - {file = "websockets-14.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3fbd68850c837e57373d95c8fe352203a512b6e49eaae4c2f4088ef8cf21980"}, - {file = "websockets-14.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b27ece32f63150c268593d5fdb82819584831a83a3f5809b7521df0685cd5d8"}, - {file = "websockets-14.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4daa0faea5424d8713142b33825fff03c736f781690d90652d2c8b053345b0e7"}, - {file = "websockets-14.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bc63cee8596a6ec84d9753fd0fcfa0452ee12f317afe4beae6b157f0070c6c7f"}, - {file = "websockets-14.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a570862c325af2111343cc9b0257b7119b904823c675b22d4ac547163088d0d"}, - {file = "websockets-14.2-cp310-cp310-win32.whl", hash = "sha256:75862126b3d2d505e895893e3deac0a9339ce750bd27b4ba515f008b5acf832d"}, - {file = "websockets-14.2-cp310-cp310-win_amd64.whl", hash = "sha256:cc45afb9c9b2dc0852d5c8b5321759cf825f82a31bfaf506b65bf4668c96f8b2"}, - {file = "websockets-14.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3bdc8c692c866ce5fefcaf07d2b55c91d6922ac397e031ef9b774e5b9ea42166"}, - {file = "websockets-14.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c93215fac5dadc63e51bcc6dceca72e72267c11def401d6668622b47675b097f"}, - {file = "websockets-14.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1c9b6535c0e2cf8a6bf938064fb754aaceb1e6a4a51a80d884cd5db569886910"}, - {file = "websockets-14.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a52a6d7cf6938e04e9dceb949d35fbdf58ac14deea26e685ab6368e73744e4c"}, - {file = "websockets-14.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f05702e93203a6ff5226e21d9b40c037761b2cfb637187c9802c10f58e40473"}, - {file = "websockets-14.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22441c81a6748a53bfcb98951d58d1af0661ab47a536af08920d129b4d1c3473"}, - {file = "websockets-14.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd9b868d78b194790e6236d9cbc46d68aba4b75b22497eb4ab64fa640c3af56"}, - {file = "websockets-14.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a5a20d5843886d34ff8c57424cc65a1deda4375729cbca4cb6b3353f3ce4142"}, - {file = "websockets-14.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:34277a29f5303d54ec6468fb525d99c99938607bc96b8d72d675dee2b9f5bf1d"}, - {file = "websockets-14.2-cp311-cp311-win32.whl", hash = "sha256:02687db35dbc7d25fd541a602b5f8e451a238ffa033030b172ff86a93cb5dc2a"}, - {file = "websockets-14.2-cp311-cp311-win_amd64.whl", hash = "sha256:862e9967b46c07d4dcd2532e9e8e3c2825e004ffbf91a5ef9dde519ee2effb0b"}, - {file = "websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c"}, - {file = "websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967"}, - {file = "websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990"}, - {file = "websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda"}, - {file = "websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95"}, - {file = "websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3"}, - {file = "websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9"}, - {file = "websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267"}, - {file = "websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe"}, - {file = "websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205"}, - {file = "websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce"}, - {file = "websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e"}, - {file = "websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad"}, - {file = "websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03"}, - {file = "websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f"}, - {file = "websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5"}, - {file = "websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a"}, - {file = "websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20"}, - {file = "websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2"}, - {file = "websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307"}, - {file = "websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc"}, - {file = "websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f"}, - {file = "websockets-14.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7cd5706caec1686c5d233bc76243ff64b1c0dc445339bd538f30547e787c11fe"}, - {file = "websockets-14.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec607328ce95a2f12b595f7ae4c5d71bf502212bddcea528290b35c286932b12"}, - {file = "websockets-14.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da85651270c6bfb630136423037dd4975199e5d4114cae6d3066641adcc9d1c7"}, - {file = "websockets-14.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3ecadc7ce90accf39903815697917643f5b7cfb73c96702318a096c00aa71f5"}, - {file = "websockets-14.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1979bee04af6a78608024bad6dfcc0cc930ce819f9e10342a29a05b5320355d0"}, - {file = "websockets-14.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dddacad58e2614a24938a50b85969d56f88e620e3f897b7d80ac0d8a5800258"}, - {file = "websockets-14.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:89a71173caaf75fa71a09a5f614f450ba3ec84ad9fca47cb2422a860676716f0"}, - {file = "websockets-14.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6af6a4b26eea4fc06c6818a6b962a952441e0e39548b44773502761ded8cc1d4"}, - {file = "websockets-14.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:80c8efa38957f20bba0117b48737993643204645e9ec45512579132508477cfc"}, - {file = "websockets-14.2-cp39-cp39-win32.whl", hash = "sha256:2e20c5f517e2163d76e2729104abc42639c41cf91f7b1839295be43302713661"}, - {file = "websockets-14.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4c8cef610e8d7c70dea92e62b6814a8cd24fbd01d7103cc89308d2bfe1659ef"}, - {file = "websockets-14.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d7d9cafbccba46e768be8a8ad4635fa3eae1ffac4c6e7cb4eb276ba41297ed29"}, - {file = "websockets-14.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:c76193c1c044bd1e9b3316dcc34b174bbf9664598791e6fb606d8d29000e070c"}, - {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd475a974d5352390baf865309fe37dec6831aafc3014ffac1eea99e84e83fc2"}, - {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6c0097a41968b2e2b54ed3424739aab0b762ca92af2379f152c1aef0187e1c"}, - {file = "websockets-14.2-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7ff794c8b36bc402f2e07c0b2ceb4a2424147ed4785ff03e2a7af03711d60a"}, - {file = "websockets-14.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dec254fcabc7bd488dab64846f588fc5b6fe0d78f641180030f8ea27b76d72c3"}, - {file = "websockets-14.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:bbe03eb853e17fd5b15448328b4ec7fb2407d45fb0245036d06a3af251f8e48f"}, - {file = "websockets-14.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a3c4aa3428b904d5404a0ed85f3644d37e2cb25996b7f096d77caeb0e96a3b42"}, - {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:577a4cebf1ceaf0b65ffc42c54856214165fb8ceeba3935852fc33f6b0c55e7f"}, - {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad1c1d02357b7665e700eca43a31d52814ad9ad9b89b58118bdabc365454b574"}, - {file = "websockets-14.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f390024a47d904613577df83ba700bd189eedc09c57af0a904e5c39624621270"}, - {file = "websockets-14.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3c1426c021c38cf92b453cdf371228d3430acd775edee6bac5a4d577efc72365"}, - {file = "websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b"}, - {file = "websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5"}, + {file = "websockets-15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5e6ee18a53dd5743e6155b8ff7e8e477c25b29b440f87f65be8165275c87fef0"}, + {file = "websockets-15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ee06405ea2e67366a661ed313e14cf2a86e84142a3462852eb96348f7219cee3"}, + {file = "websockets-15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8711682a629bbcaf492f5e0af72d378e976ea1d127a2d47584fa1c2c080b436b"}, + {file = "websockets-15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94c4a9b01eede952442c088d415861b0cf2053cbd696b863f6d5022d4e4e2453"}, + {file = "websockets-15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45535fead66e873f411c1d3cf0d3e175e66f4dd83c4f59d707d5b3e4c56541c4"}, + {file = "websockets-15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e389efe46ccb25a1f93d08c7a74e8123a2517f7b7458f043bd7529d1a63ffeb"}, + {file = "websockets-15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:67a04754d121ea5ca39ddedc3f77071651fb5b0bc6b973c71c515415b44ed9c5"}, + {file = "websockets-15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bd66b4865c8b853b8cca7379afb692fc7f52cf898786537dfb5e5e2d64f0a47f"}, + {file = "websockets-15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a4cc73a6ae0a6751b76e69cece9d0311f054da9b22df6a12f2c53111735657c8"}, + {file = "websockets-15.0-cp310-cp310-win32.whl", hash = "sha256:89da58e4005e153b03fe8b8794330e3f6a9774ee9e1c3bd5bc52eb098c3b0c4f"}, + {file = "websockets-15.0-cp310-cp310-win_amd64.whl", hash = "sha256:4ff380aabd7a74a42a760ee76c68826a8f417ceb6ea415bd574a035a111fd133"}, + {file = "websockets-15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dd24c4d256558429aeeb8d6c24ebad4e982ac52c50bc3670ae8646c181263965"}, + {file = "websockets-15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f83eca8cbfd168e424dfa3b3b5c955d6c281e8fc09feb9d870886ff8d03683c7"}, + {file = "websockets-15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4095a1f2093002c2208becf6f9a178b336b7572512ee0a1179731acb7788e8ad"}, + {file = "websockets-15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb915101dfbf318486364ce85662bb7b020840f68138014972c08331458d41f3"}, + {file = "websockets-15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45d464622314973d78f364689d5dbb9144e559f93dca11b11af3f2480b5034e1"}, + {file = "websockets-15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace960769d60037ca9625b4c578a6f28a14301bd2a1ff13bb00e824ac9f73e55"}, + {file = "websockets-15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7cd4b1015d2f60dfe539ee6c95bc968d5d5fad92ab01bb5501a77393da4f596"}, + {file = "websockets-15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f7290295794b5dec470867c7baa4a14182b9732603fd0caf2a5bf1dc3ccabf3"}, + {file = "websockets-15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3abd670ca7ce230d5a624fd3d55e055215d8d9b723adee0a348352f5d8d12ff4"}, + {file = "websockets-15.0-cp311-cp311-win32.whl", hash = "sha256:110a847085246ab8d4d119632145224d6b49e406c64f1bbeed45c6f05097b680"}, + {file = "websockets-15.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7bbbe2cd6ed80aceef2a14e9f1c1b61683194c216472ed5ff33b700e784e37"}, + {file = "websockets-15.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cccc18077acd34c8072578394ec79563664b1c205f7a86a62e94fafc7b59001f"}, + {file = "websockets-15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4c22992e24f12de340ca5f824121a5b3e1a37ad4360b4e1aaf15e9d1c42582d"}, + {file = "websockets-15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1206432cc6c644f6fc03374b264c5ff805d980311563202ed7fef91a38906276"}, + {file = "websockets-15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3cc75ef3e17490042c47e0523aee1bcc4eacd2482796107fd59dd1100a44bc"}, + {file = "websockets-15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b89504227a5311610e4be16071465885a0a3d6b0e82e305ef46d9b064ce5fb72"}, + {file = "websockets-15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56e3efe356416bc67a8e093607315951d76910f03d2b3ad49c4ade9207bf710d"}, + {file = "websockets-15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f2205cdb444a42a7919690238fb5979a05439b9dbb73dd47c863d39640d85ab"}, + {file = "websockets-15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aea01f40995fa0945c020228ab919b8dfc93fc8a9f2d3d705ab5b793f32d9e99"}, + {file = "websockets-15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9f8e33747b1332db11cf7fcf4a9512bef9748cb5eb4d3f7fbc8c30d75dc6ffc"}, + {file = "websockets-15.0-cp312-cp312-win32.whl", hash = "sha256:32e02a2d83f4954aa8c17e03fe8ec6962432c39aca4be7e8ee346b05a3476904"}, + {file = "websockets-15.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc02b159b65c05f2ed9ec176b715b66918a674bd4daed48a9a7a590dd4be1aa"}, + {file = "websockets-15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d2244d8ab24374bed366f9ff206e2619345f9cd7fe79aad5225f53faac28b6b1"}, + {file = "websockets-15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a302241fbe825a3e4fe07666a2ab513edfdc6d43ce24b79691b45115273b5e7"}, + {file = "websockets-15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:10552fed076757a70ba2c18edcbc601c7637b30cdfe8c24b65171e824c7d6081"}, + {file = "websockets-15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c53f97032b87a406044a1c33d1e9290cc38b117a8062e8a8b285175d7e2f99c9"}, + {file = "websockets-15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1caf951110ca757b8ad9c4974f5cac7b8413004d2f29707e4d03a65d54cedf2b"}, + {file = "websockets-15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf1ab71f9f23b0a1d52ec1682a3907e0c208c12fef9c3e99d2b80166b17905f"}, + {file = "websockets-15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bfcd3acc1a81f106abac6afd42327d2cf1e77ec905ae11dc1d9142a006a496b6"}, + {file = "websockets-15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8c5c8e1bac05ef3c23722e591ef4f688f528235e2480f157a9cfe0a19081375"}, + {file = "websockets-15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:86bfb52a9cfbcc09aba2b71388b0a20ea5c52b6517c0b2e316222435a8cdab72"}, + {file = "websockets-15.0-cp313-cp313-win32.whl", hash = "sha256:26ba70fed190708551c19a360f9d7eca8e8c0f615d19a574292b7229e0ae324c"}, + {file = "websockets-15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8"}, + {file = "websockets-15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c348abc5924caa02a62896300e32ea80a81521f91d6db2e853e6b1994017c9f6"}, + {file = "websockets-15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5294fcb410ed0a45d5d1cdedc4e51a60aab5b2b3193999028ea94afc2f554b05"}, + {file = "websockets-15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c24ba103ecf45861e2e1f933d40b2d93f5d52d8228870c3e7bf1299cd1cb8ff1"}, + {file = "websockets-15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc8821a03bcfb36e4e4705316f6b66af28450357af8a575dc8f4b09bf02a3dee"}, + {file = "websockets-15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc5ae23ada6515f31604f700009e2df90b091b67d463a8401c1d8a37f76c1d7"}, + {file = "websockets-15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ac67b542505186b3bbdaffbc303292e1ee9c8729e5d5df243c1f20f4bb9057e"}, + {file = "websockets-15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c86dc2068f1c5ca2065aca34f257bbf4f78caf566eb230f692ad347da191f0a1"}, + {file = "websockets-15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:30cff3ef329682b6182c01c568f551481774c476722020b8f7d0daacbed07a17"}, + {file = "websockets-15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98dcf978d4c6048965d1762abd534c9d53bae981a035bfe486690ba11f49bbbb"}, + {file = "websockets-15.0-cp39-cp39-win32.whl", hash = "sha256:37d66646f929ae7c22c79bc73ec4074d6db45e6384500ee3e0d476daf55482a9"}, + {file = "websockets-15.0-cp39-cp39-win_amd64.whl", hash = "sha256:24d5333a9b2343330f0f4eb88546e2c32a7f5c280f8dd7d3cc079beb0901781b"}, + {file = "websockets-15.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b499caef4bca9cbd0bd23cd3386f5113ee7378094a3cb613a2fa543260fe9506"}, + {file = "websockets-15.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:17f2854c6bd9ee008c4b270f7010fe2da6c16eac5724a175e75010aacd905b31"}, + {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89f72524033abbfde880ad338fd3c2c16e31ae232323ebdfbc745cbb1b3dcc03"}, + {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1657a9eecb29d7838e3b415458cc494e6d1b194f7ac73a34aa55c6fb6c72d1f3"}, + {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e413352a921f5ad5d66f9e2869b977e88d5103fc528b6deb8423028a2befd842"}, + {file = "websockets-15.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8561c48b0090993e3b2a54db480cab1d23eb2c5735067213bb90f402806339f5"}, + {file = "websockets-15.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:190bc6ef8690cd88232a038d1b15714c258f79653abad62f7048249b09438af3"}, + {file = "websockets-15.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:327adab7671f3726b0ba69be9e865bba23b37a605b585e65895c428f6e47e766"}, + {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd8ef197c87afe0a9009f7a28b5dc613bfc585d329f80b7af404e766aa9e8c7"}, + {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:789c43bf4a10cd067c24c321238e800b8b2716c863ddb2294d2fed886fa5a689"}, + {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7394c0b7d460569c9285fa089a429f58465db930012566c03046f9e3ab0ed181"}, + {file = "websockets-15.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ea4f210422b912ebe58ef0ad33088bc8e5c5ff9655a8822500690abc3b1232d"}, + {file = "websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3"}, + {file = "websockets-15.0.tar.gz", hash = "sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab"}, ] [[package]] @@ -3645,7 +3646,7 @@ files = [ ] [package.extras] -dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] +dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [[package]] name = "wsproto" @@ -3664,20 +3665,20 @@ h11 = ">=0.9.0,<1" [[package]] name = "yt-dlp" -version = "2025.1.26" +version = "2025.2.19" description = "A feature-rich command-line audio/video downloader" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "yt_dlp-2025.1.26-py3-none-any.whl", hash = "sha256:3e76bd896b9f96601021ca192ca0fbdd195e3c3dcc28302a3a34c9bc4979da7b"}, - {file = "yt_dlp-2025.1.26.tar.gz", hash = "sha256:1c9738266921ad43c568ad01ac3362fb7c7af549276fbec92bd72f140da16240"}, + {file = "yt_dlp-2025.2.19-py3-none-any.whl", hash = "sha256:3ed218eaeece55e9d715afd41abc450dc406ee63bf79355169dfde312d38fdb8"}, + {file = "yt_dlp-2025.2.19.tar.gz", hash = "sha256:f33ca76df2e4db31880f2fe408d44f5058d9f135015b13e50610dfbe78245bea"}, ] [package.extras] build = ["build", "hatchling", "pip", "setuptools (>=71.0.2)", "wheel"] -curl-cffi = ["curl-cffi (==0.5.10)", "curl-cffi (>=0.5.10,!=0.6.*,<0.7.2)"] -default = ["brotli", "brotlicffi", "certifi", "mutagen", "pycryptodomex", "requests (>=2.32.2,<3)", "urllib3 (>=1.26.17,<3)", "websockets (>=13.0)"] +curl-cffi = ["curl-cffi (==0.5.10) ; os_name == \"nt\" and implementation_name == \"cpython\"", "curl-cffi (>=0.5.10,!=0.6.*,<0.7.2) ; os_name != \"nt\" and implementation_name == \"cpython\""] +default = ["brotli ; implementation_name == \"cpython\"", "brotlicffi ; implementation_name != \"cpython\"", "certifi", "mutagen", "pycryptodomex", "requests (>=2.32.2,<3)", "urllib3 (>=1.26.17,<3)", "websockets (>=13.0)"] dev = ["autopep8 (>=2.0,<3.0)", "pre-commit", "pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)", "ruff (>=0.9.0,<0.10.0)"] pyinstaller = ["pyinstaller (>=6.11.1)"] secretstorage = ["cffi", "secretstorage"] From 5c30f263940961495bb1f64c05a348ee161fa3ab Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:10:06 -0600 Subject: [PATCH 04/40] Create CODEOWNERS --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 CODEOWNERS diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..b5ffe06 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @msramalho From 9cf449a68f74728fa52563035095fd797cbc0e24 Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:14:00 -0600 Subject: [PATCH 05/40] Update .gitignore --- .gitignore | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.gitignore b/.gitignore index 15bb465..ebe19c0 100644 --- a/.gitignore +++ b/.gitignore @@ -113,13 +113,6 @@ ipython_config.py # pyenv .python-version -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ From d9f36957cd52c604ba0b7b3692e8720a53ef1b94 Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Wed, 26 Feb 2025 10:40:04 -0600 Subject: [PATCH 06/40] Update `Makefile` (#55) * Update Makefile * Update pyproject.toml * Update Makefile * Update pyproject.toml --- Makefile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 2fd462c..bfcb3d3 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,23 @@ +.PHONY: clean-dev clean-dev: @echo -n "Are you sure? [yes/N] (this will delete volumes) " && read ans && [ $${ans:-N} = yes ] docker compose -f docker-compose.yml -f docker-compose.dev.yml down --volumes --remove-orphans +.PHONY: dev dev: docker compose --env-file .env.dev -f docker-compose.yml -f docker-compose.dev.yml build docker compose --env-file .env.dev -f docker-compose.yml -f docker-compose.dev.yml up --remove-orphans - +.PHONY: dev-redis-only dev-redis-only: docker compose --env-file .env.dev -f docker-compose.yml -f docker-compose.dev.yml build redis docker compose --env-file .env.dev -f docker-compose.yml -f docker-compose.dev.yml up --remove-orphans redis +.PHONY: stop-dev stop-dev: docker compose -f docker-compose.yml -f docker-compose.dev.yml down --volumes +.PHONY: prod prod: docker compose --env-file .env.prod build docker compose --env-file .env.prod up -d --remove-orphans @@ -21,5 +25,6 @@ prod: docker image prune -f docker system df +.PHONY: stop-prod stop-prod: - docker compose down \ No newline at end of file + docker compose down From 7e48f706df3902818b6ed5b4334d1f3b908d89f6 Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Wed, 26 Feb 2025 10:42:28 -0600 Subject: [PATCH 07/40] Add `pre-commit` with GiHub Action (#56) * Update pyproject.toml * add pre-commit * Create .pre-commit-config.yaml * Comment out ruff * Update .pre-commit-config.yaml * General formatting * Create format-and-fail.yml * Update ci.yml * Add pre-commit to dev dependencies * Update pyproject.toml --- .env.alembic | 2 +- .env.example | 2 +- .env.test | 2 +- .github/workflows/ci.yml | 10 +- .github/workflows/format-and-fail.yml | 16 +++ .pre-commit-config.yaml | 79 +++++++++++ LICENSE | 2 +- Makefile | 4 + README.md | 2 +- app/migrations/env.py | 4 +- ...7ed0_create_archives_store_until_column.py | 2 +- ...24ec4b1_rename_sheets_last_archived_col.py | 2 +- ...dd_new_service_account_email_column_to_.py | 2 +- ...21d2c96d8_add_sheet_id_to_archive_table.py | 2 +- ...45b_modify_archive_url_to_have_uuid_id_.py | 3 +- .../a23aaf3ae930_drop_active_column.py | 2 +- ...a012ec405b8_add_columns_to_groups_table.py | 2 +- app/shared/aa_utils.py | 5 +- app/shared/business_logic.py | 1 + app/shared/db/database.py | 14 +- app/shared/db/models.py | 15 +- app/shared/db/worker_crud.py | 6 +- app/shared/log.py | 3 +- app/shared/schemas.py | 3 +- app/shared/settings.py | 15 +- app/shared/task_messaging.py | 5 +- app/shared/user_groups.py | 11 +- app/shared/utils/misc.py | 2 +- app/tests/conftest.py | 23 ++-- app/tests/fake_service_account.json | 4 +- app/tests/orchestration.test.yaml | 2 +- app/tests/shared/db/test_models.py | 2 +- app/tests/shared/db/test_worker_crud.py | 11 +- app/tests/shared/test_business_logic.py | 7 +- app/tests/shared/utils/test_misc.py | 4 +- app/tests/user-groups.test.broken.yaml | 2 +- app/tests/user-groups.test.yaml | 2 +- app/tests/web/db/test_crud.py | 7 +- app/tests/web/db/test_user_state.py | 1 + app/tests/web/endpoints/test_default.py | 6 +- .../web/endpoints/test_interoperability.py | 2 +- app/tests/web/endpoints/test_sheet.py | 4 +- app/tests/web/test_main.py | 10 +- app/tests/web/test_security.py | 4 +- app/tests/worker/test_worker_main.py | 7 +- app/web/__init__.py | 3 +- app/web/config.py | 2 +- app/web/db/crud.py | 18 +-- app/web/db/user_state.py | 11 +- app/web/endpoints/default.py | 8 +- app/web/endpoints/interoperability.py | 14 +- app/web/endpoints/sheet.py | 12 +- app/web/endpoints/task.py | 4 +- app/web/endpoints/url.py | 20 +-- app/web/events.py | 24 +++- app/web/main.py | 27 ++-- app/web/middleware.py | 8 +- app/web/security.py | 15 +- app/web/utils/metrics.py | 3 +- app/web/utils/misc.py | 1 + app/worker/main.py | 19 +-- app/worker/worker_log.py | 8 +- docker-compose.dev.yml | 2 +- docker-compose.yml | 6 +- poetry.lock | 129 +++++++++++++++++- pyproject.toml | 2 +- user-groups.example.yaml | 1 - worker.Dockerfile | 2 +- 68 files changed, 473 insertions(+), 182 deletions(-) create mode 100644 .github/workflows/format-and-fail.yml create mode 100644 .pre-commit-config.yaml diff --git a/.env.alembic b/.env.alembic index 8691557..11bf2aa 100644 --- a/.env.alembic +++ b/.env.alembic @@ -2,4 +2,4 @@ CHROME_APP_IDS='["1234567890"]' ALLOWED_ORIGINS='["allowed"]' BLOCKED_EMAILS='[]' DATABASE_PATH="sqlite:///./database/auto-archiver.db" -API_BEARER_TOKEN=THIS_API_TOKEN_SHOULD_NEVER_BE_USED \ No newline at end of file +API_BEARER_TOKEN=THIS_API_TOKEN_SHOULD_NEVER_BE_USED diff --git a/.env.example b/.env.example index ef3935a..ea544ef 100644 --- a/.env.example +++ b/.env.example @@ -35,4 +35,4 @@ MAIL_SSL_TLS=True # celery workers config -CONCURRENCY=2 \ No newline at end of file +CONCURRENCY=2 diff --git a/.env.test b/.env.test index 32318f0..360f40e 100644 --- a/.env.test +++ b/.env.test @@ -5,4 +5,4 @@ BLOCKED_EMAILS='["blocked@example.com"]' DATABASE_PATH="sqlite:///auto-archiver.test.db" API_BEARER_TOKEN=this_is_the_test_api_token -USER_GROUPS_FILENAME=app/tests/user-groups.test.yaml \ No newline at end of file +USER_GROUPS_FILENAME=app/tests/user-groups.test.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b63544..4135ac2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,13 +1,9 @@ name: CI on: push: - branches: - - main - - dev + branches: [ main, dev ] pull_request: - branches: - - main - - dev + branches: [ main, dev ] jobs: test: @@ -41,4 +37,4 @@ jobs: run: poetry run coverage run -m pytest -v -ra --color=yes app/tests/ - name: Report coverage - run: poetry run coverage report \ No newline at end of file + run: poetry run coverage report diff --git a/.github/workflows/format-and-fail.yml b/.github/workflows/format-and-fail.yml new file mode 100644 index 0000000..f1c01f8 --- /dev/null +++ b/.github/workflows/format-and-fail.yml @@ -0,0 +1,16 @@ +name: Format and Fail +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: "3.11" + - uses: pre-commit/action@v3.0.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..73765d5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,79 @@ +repos: + - repo: https://github.com/nbQA-dev/nbQA + rev: 1.8.5 + hooks: + - id: nbqa-ruff + args: + - --fix + - --target-version=py311 + - --ignore=E721,E722 + - --line-length=80 + - id: nbqa-black + args: + - --line-length=80 + - id: nbqa-isort + args: + - --float-to-top + - --profile=black + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-json + - id: check-case-conflict + - id: check-toml + - id: check-merge-conflict + - id: check-xml + - id: check-yaml + exclude: app/tests/user-groups.test.broken.yaml + - id: end-of-file-fixer + - id: check-symlinks + - id: mixed-line-ending + - id: sort-simple-yaml + - id: fix-encoding-pragma + args: + - --remove + - id: pretty-format-json + args: + - --autofix + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: python-check-blanket-noqa + - id: python-check-mock-methods + - id: python-no-eval + - id: python-no-log-warn + + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + name: Run isort to sort imports + files: \.py$ + # To keep consistent with the global isort skip config defined in setup.cfg + exclude: ^build/.*$|^.tox/.*$|^venv/.*$ + args: + - --lines-after-imports=2 + - --profile=black + - --line-length=80 + +# - repo: https://github.com/astral-sh/ruff-pre-commit +# rev: v0.4.10 +# hooks: +# - id: ruff +# types_or: [python,pyi] +# args: +# - --fix +# - --target-version=py311 +# - --select=B,C,E,F,W,B9 +# - --line-length=80 +# - --ignore=E203,E402,E501,E261 +# - id: ruff-format +# types_or: [ python,pyi] +# args: +# - --target-version=py311 +# - --line-length=80 diff --git a/LICENSE b/LICENSE index e10dcd9..c5bae4c 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/Makefile b/Makefile index bfcb3d3..ddda086 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +.PHONY: lint +lint: + poetry run pre-commit run --all-files + .PHONY: clean-dev clean-dev: @echo -n "Are you sure? [yes/N] (this will delete volumes) " && read ans && [ $${ans:-N} = yes ] diff --git a/README.md b/README.md index 2c87342..da402e3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ To properly set up the API you need to install `docker` and to have these files, 2. go through the example file and configure it according to your needs. 3. you will need to create and reference at least one `secrets/orchestration.yaml` file, you can do so by following the instructions in the [auto-archiver](https://github.com/bellingcat/auto-archiver#installation) that automatically generates one for you. If you use the archive sheets feature you will need to create a `orchestrationsheets-sheets.yaml` file as well that should have the `gsheet_feeder` and `gsheet_db` enabled and configured, the auto-archiver has [extensive documentation](https://auto-archiver.readthedocs.io/en/latest/) on how to set this up. -Do not commit those files, they are .gitignored by default. +Do not commit those files, they are .gitignored by default. We also advise you to keep any sensitive files in the `secrets/` folder which is pinned and gitignored. We have examples for both of those files (`.env.example` and `user-groups.example.yaml`), and here's how to set them up whether you're in development or production: diff --git a/app/migrations/env.py b/app/migrations/env.py index 870ef18..1f579ab 100644 --- a/app/migrations/env.py +++ b/app/migrations/env.py @@ -1,11 +1,11 @@ from logging.config import fileConfig -from sqlalchemy import engine_from_config -from sqlalchemy import pool from alembic import context +from sqlalchemy import engine_from_config, pool from app.shared.settings import get_settings + # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config diff --git a/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py b/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py index d00fa2c..8642f2b 100644 --- a/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py +++ b/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py @@ -5,8 +5,8 @@ Revises: 1636724ec4b1 Create Date: 2025-02-08 15:22:20.392522 """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. diff --git a/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py b/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py index 6c109f3..324f75a 100644 --- a/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py +++ b/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py @@ -5,8 +5,8 @@ Revises: a23aaf3ae930 Create Date: 2025-02-05 19:19:01.984396 """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. diff --git a/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py b/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py index 7067746..572905d 100644 --- a/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py +++ b/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py @@ -5,8 +5,8 @@ Revises: 02b2f6d17ed0 Create Date: 2025-02-11 21:53:23.293274 """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. diff --git a/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py b/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py index 3011cf6..892e853 100644 --- a/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py +++ b/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py @@ -5,8 +5,8 @@ Revises: fa012ec405b8 Create Date: 2024-11-04 11:12:30.237299 """ -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.engine.reflection import Inspector diff --git a/app/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py b/app/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py index a2b708a..bdfe474 100644 --- a/app/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py +++ b/app/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py @@ -1,12 +1,13 @@ """modify archive url to have uuid id instead of url unique constraint Revision ID: 9369a264945b -Revises: +Revises: Create Date: 2023-12-20 17:24:59.320691 """ from alembic import op + # revision identifiers, used by Alembic. revision = '9369a264945b' down_revision = None diff --git a/app/migrations/versions/a23aaf3ae930_drop_active_column.py b/app/migrations/versions/a23aaf3ae930_drop_active_column.py index 912f408..ebc85e7 100644 --- a/app/migrations/versions/a23aaf3ae930_drop_active_column.py +++ b/app/migrations/versions/a23aaf3ae930_drop_active_column.py @@ -5,8 +5,8 @@ Revises: 89121d2c96d8 Create Date: 2025-02-04 12:19:20.753570 """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. diff --git a/app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py b/app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py index f0577ea..c3169c3 100644 --- a/app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py +++ b/app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py @@ -5,8 +5,8 @@ Revises: 93a611e4c066 Create Date: 2024-10-31 09:36:50.360710 """ -from alembic import op import sqlalchemy as sa +from alembic import op from sqlalchemy.engine.reflection import Inspector diff --git a/app/shared/aa_utils.py b/app/shared/aa_utils.py index 393a975..466d003 100644 --- a/app/shared/aa_utils.py +++ b/app/shared/aa_utils.py @@ -1,11 +1,13 @@ # TODO: code in this file should eventually be moved to the auto-archiver code base from typing import List -from loguru import logger + from auto_archiver.core import Media, Metadata +from loguru import logger from app.shared.db import models + def get_all_urls(result: Metadata) -> List[models.ArchiveUrl]: db_urls = [] for m in result.media: @@ -29,4 +31,3 @@ def convert_if_media(media): except Exception as e: logger.debug(f"error parsing {media} : {e}") return False - diff --git a/app/shared/business_logic.py b/app/shared/business_logic.py index d179fda..b4291b6 100644 --- a/app/shared/business_logic.py +++ b/app/shared/business_logic.py @@ -2,6 +2,7 @@ import datetime + from sqlalchemy.orm import Session from app.shared.db import worker_crud diff --git a/app/shared/db/database.py b/app/shared/db/database.py index 171b97b..51c235a 100644 --- a/app/shared/db/database.py +++ b/app/shared/db/database.py @@ -1,8 +1,14 @@ -from functools import lru_cache -from sqlalchemy import Engine, create_engine, event, text -from sqlalchemy.orm import sessionmaker from contextlib import asynccontextmanager, contextmanager -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, AsyncEngine, async_sessionmaker +from functools import lru_cache + +from sqlalchemy import Engine, create_engine, event, text +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) +from sqlalchemy.orm import sessionmaker from app.shared.settings import get_settings diff --git a/app/shared/db/models.py b/app/shared/db/models.py index 1736224..aa93034 100644 --- a/app/shared/db/models.py +++ b/app/shared/db/models.py @@ -1,8 +1,17 @@ -from sqlalchemy import Column, String, JSON, DateTime, Boolean, Table, ForeignKey -from sqlalchemy.sql import func -from sqlalchemy.orm import relationship, declarative_base import uuid +from sqlalchemy import ( + JSON, + Boolean, + Column, + DateTime, + ForeignKey, + String, + Table, +) +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.sql import func + Base = declarative_base() diff --git a/app/shared/db/worker_crud.py b/app/shared/db/worker_crud.py index 814689a..82f3380 100644 --- a/app/shared/db/worker_crud.py +++ b/app/shared/db/worker_crud.py @@ -1,8 +1,10 @@ -from sqlalchemy.orm import Session from datetime import datetime -from app.shared.db import models +from sqlalchemy.orm import Session + from app.shared import schemas +from app.shared.db import models + # TODO: isolate database operations away from worker and into WEB # ONLY WORKER diff --git a/app/shared/log.py b/app/shared/log.py index 68587e2..734c368 100644 --- a/app/shared/log.py +++ b/app/shared/log.py @@ -1,4 +1,5 @@ import traceback + from loguru import logger @@ -6,7 +7,7 @@ from loguru import logger logger.add("logs/api_logs.log", retention="30 days") logger.add("logs/error_logs.log", retention="30 days", level="ERROR") - + def log_error(e: Exception, traceback_str: str = None, extra:str = ""): if not traceback_str: traceback_str = traceback.format_exc() if extra: extra = f"{extra}\n" diff --git a/app/shared/schemas.py b/app/shared/schemas.py index 66119f7..e8479f5 100644 --- a/app/shared/schemas.py +++ b/app/shared/schemas.py @@ -1,7 +1,8 @@ +from datetime import datetime from typing import Annotated + from annotated_types import Len from pydantic import BaseModel -from datetime import datetime class SubmitSheet(BaseModel): diff --git a/app/shared/settings.py b/app/shared/settings.py index d884f80..2f68ae7 100644 --- a/app/shared/settings.py +++ b/app/shared/settings.py @@ -1,14 +1,15 @@ -from functools import lru_cache import os +from functools import lru_cache +from typing import Annotated, Set + +from annotated_types import Len from fastapi_mail import ConnectionConfig from pydantic_settings import BaseSettings, SettingsConfigDict -from typing import Annotated, Set -from annotated_types import Len class Settings(BaseSettings): - + model_config = SettingsConfigDict(env_file=os.environ.get("ENVIRONMENT_FILE") , env_file_encoding='utf-8', extra='ignore', str_strip_whitespace=True) # general @@ -37,14 +38,14 @@ class Settings(BaseSettings): if self.REDIS_PASSWORD: return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOSTNAME}:6379" return f"redis://{self.REDIS_HOSTNAME}:6379" - + # cronjobs CRON_ARCHIVE_SHEETS: bool = False CRON_DELETE_STALE_SHEETS: bool = False DELETE_STALE_SHEETS_DAYS: int = 14 CRON_DELETE_SCHEDULED_ARCHIVES: bool = False DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS: int = 7 - + # observability REPEAT_COUNT_METRICS_SECONDS: int = 30 @@ -73,4 +74,4 @@ class Settings(BaseSettings): @lru_cache def get_settings(): - return Settings() \ No newline at end of file + return Settings() diff --git a/app/shared/task_messaging.py b/app/shared/task_messaging.py index 21fb3d1..88d0057 100644 --- a/app/shared/task_messaging.py +++ b/app/shared/task_messaging.py @@ -1,8 +1,9 @@ from functools import lru_cache -from celery import Celery -import redis +from celery import Celery + +import redis from app.shared.settings import get_settings diff --git a/app/shared/user_groups.py b/app/shared/user_groups.py index 592e012..480b84e 100644 --- a/app/shared/user_groups.py +++ b/app/shared/user_groups.py @@ -1,9 +1,16 @@ import json import os +from typing import Dict, List, Set + import yaml from loguru import logger -from pydantic import BaseModel, computed_field, field_validator, Field, model_validator -from typing import Dict, List, Set +from pydantic import ( + BaseModel, + Field, + computed_field, + field_validator, + model_validator, +) from typing_extensions import Self diff --git a/app/shared/utils/misc.py b/app/shared/utils/misc.py index 562b2c3..6c5940d 100644 --- a/app/shared/utils/misc.py +++ b/app/shared/utils/misc.py @@ -7,4 +7,4 @@ def fnv1a_hash_mod(s: str, modulo:int) -> int: hash ^= ord(char) hash *= fnv_prime hash &= 0xFFFFFFFF # Keep it 32-bit - return (hash if hash < 0x80000000 else hash - 0x100000000) % modulo \ No newline at end of file + return (hash if hash < 0x80000000 else hash - 0x100000000) % modulo diff --git a/app/tests/conftest.py b/app/tests/conftest.py index afa76f9..f7da39e 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -1,12 +1,14 @@ import os from typing import AsyncGenerator -from fastapi.testclient import TestClient -import pytest from unittest.mock import patch + +import pytest import pytest_asyncio -from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine -from app.web.config import ALLOW_ANY_EMAIL +from fastapi.testclient import TestClient +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession + from app.shared.settings import Settings +from app.web.config import ALLOW_ANY_EMAIL from app.web.db.user_state import UserState @@ -65,10 +67,11 @@ def db_session(test_db): @pytest_asyncio.fixture() async def async_test_db(get_settings: Settings): + import asyncio + from app.shared.db import models from app.shared.db.database import make_async_engine from app.web.db.crud import get_user_group_names - import asyncio get_user_group_names.cache_clear() engine = await make_async_engine(get_settings.ASYNC_DATABASE_PATH) @@ -108,8 +111,8 @@ async def async_db_session(async_test_db: AsyncEngine) -> AsyncGenerator[AsyncSe @pytest.fixture() def app(db_session): - from app.web.main import app_factory from app.web.db import crud + from app.web.main import app_factory app = app_factory() crud.upsert_user_groups(db_session) return app @@ -123,7 +126,11 @@ def client(app): @pytest.fixture() def app_with_auth(app, db_session): - from app.web.security import get_token_or_user_auth, get_user_auth, get_user_state + from app.web.security import ( + get_token_or_user_auth, + get_user_auth, + get_user_state, + ) app.dependency_overrides[get_token_or_user_auth] = lambda: "rick@example.com" app.dependency_overrides[get_user_auth] = lambda: "morty@example.com" app.dependency_overrides[get_user_state] = lambda: UserState(db_session, "MORTY@example.com") @@ -138,7 +145,7 @@ def client_with_auth(app_with_auth): @pytest.fixture() def app_with_token(app): - from app.web.security import token_api_key_auth, get_token_or_user_auth + from app.web.security import get_token_or_user_auth, token_api_key_auth app.dependency_overrides[token_api_key_auth] = lambda: ALLOW_ANY_EMAIL app.dependency_overrides[get_token_or_user_auth] = lambda: ALLOW_ANY_EMAIL return app diff --git a/app/tests/fake_service_account.json b/app/tests/fake_service_account.json index 3d41bd9..10c0585 100644 --- a/app/tests/fake_service_account.json +++ b/app/tests/fake_service_account.json @@ -1,3 +1,3 @@ { - "client_email": "fake_service_account@fake_service_account.iam.gserviceaccount.com" -} \ No newline at end of file + "client_email": "fake_service_account@fake_service_account.iam.gserviceaccount.com" +} diff --git a/app/tests/orchestration.test.yaml b/app/tests/orchestration.test.yaml index 4ee1880..ef7ed27 100644 --- a/app/tests/orchestration.test.yaml +++ b/app/tests/orchestration.test.yaml @@ -15,7 +15,7 @@ configurations: gsheet_feeder: service_account: "app/tests/fake_service_account.json" cli_feeder: - urls: + urls: - "url1" hash_enricher: algorithm: "SHA-256" diff --git a/app/tests/shared/db/test_models.py b/app/tests/shared/db/test_models.py index 35ba368..4da9571 100644 --- a/app/tests/shared/db/test_models.py +++ b/app/tests/shared/db/test_models.py @@ -3,4 +3,4 @@ def test_generate_uuid(): assert generate_uuid() != generate_uuid() assert len(generate_uuid()) == 36 - assert generate_uuid().count("-") == 4 \ No newline at end of file + assert generate_uuid().count("-") == 4 diff --git a/app/tests/shared/db/test_worker_crud.py b/app/tests/shared/db/test_worker_crud.py index 1098cbe..09e9a76 100644 --- a/app/tests/shared/db/test_worker_crud.py +++ b/app/tests/shared/db/test_worker_crud.py @@ -1,10 +1,9 @@ -from app.shared.db import models -from app.shared.db import worker_crud, models from datetime import datetime - +from app.shared.db import models, worker_crud from app.tests.web.db.test_crud import test_data + def test_update_sheet_last_url_archived_at(db_session): # Create test sheet @@ -19,7 +18,7 @@ def test_update_sheet_last_url_archived_at(db_session): db_session.refresh(test_sheet) assert isinstance(test_sheet.last_url_archived_at, datetime) assert test_sheet.last_url_archived_at > before - + # Test non-existent sheet assert worker_crud.update_sheet_last_url_archived_at(db_session, "non-existent-sheet") is False @@ -73,8 +72,8 @@ def test_create_tag(db_session): def test_create_task(db_session): - from app.shared.db import worker_crud from app.shared import schemas + from app.shared.db import worker_crud task = schemas.ArchiveCreate( id="archive-id-456-101", @@ -114,4 +113,4 @@ def test_create_task(db_session): assert nt.group_id == "spaceship" assert len(nt.tags) == 0 assert len(nt.urls) == 0 - assert nt.created_at is not None \ No newline at end of file + assert nt.created_at is not None diff --git a/app/tests/shared/test_business_logic.py b/app/tests/shared/test_business_logic.py index 225fb11..830fa7b 100644 --- a/app/tests/shared/test_business_logic.py +++ b/app/tests/shared/test_business_logic.py @@ -1,7 +1,12 @@ from datetime import datetime, timedelta from unittest.mock import MagicMock, patch + import pytest -from app.shared.business_logic import get_store_archive_until, get_store_archive_until_or_never + +from app.shared.business_logic import ( + get_store_archive_until, + get_store_archive_until_or_never, +) class Test_get_store_archive_until: diff --git a/app/tests/shared/utils/test_misc.py b/app/tests/shared/utils/test_misc.py index d7595c8..18db28d 100644 --- a/app/tests/shared/utils/test_misc.py +++ b/app/tests/shared/utils/test_misc.py @@ -11,7 +11,7 @@ def test_fnv1a_hash_mod(): # Test different modulos hash1 = fnv1a_hash_mod("test", 5) - hash2 = fnv1a_hash_mod("test", 10) + hash2 = fnv1a_hash_mod("test", 10) assert 0 <= hash1 < 5 assert 0 <= hash2 < 10 @@ -28,4 +28,4 @@ def test_fnv1a_hash_mod(): assert 0 <= fnv1a_hash_mod("测试", 10) < 10 # Test modulo = 1 edge case - assert fnv1a_hash_mod("test", 1) == 0 \ No newline at end of file + assert fnv1a_hash_mod("test", 1) == 0 diff --git a/app/tests/user-groups.test.broken.yaml b/app/tests/user-groups.test.broken.yaml index 8bc59c5..9b41741 100644 --- a/app/tests/user-groups.test.broken.yaml +++ b/app/tests/user-groups.test.broken.yaml @@ -3,4 +3,4 @@ This is just an invalid yaml for testing still broken: True - one - - two \ No newline at end of file + - two diff --git a/app/tests/user-groups.test.yaml b/app/tests/user-groups.test.yaml index 16a3ba7..e9a446f 100644 --- a/app/tests/user-groups.test.yaml +++ b/app/tests/user-groups.test.yaml @@ -84,4 +84,4 @@ groups: # max_archive_lifespan_months: 12 max_monthly_urls: 1 # max_monthly_mbs: 50 - priority: "low" \ No newline at end of file + priority: "low" diff --git a/app/tests/web/db/test_crud.py b/app/tests/web/db/test_crud.py index aad9d4c..298a087 100644 --- a/app/tests/web/db/test_crud.py +++ b/app/tests/web/db/test_crud.py @@ -3,10 +3,12 @@ from unittest.mock import patch import pytest import yaml + from app.shared.db import models from app.shared.settings import Settings - from app.web.db import crud + + authors = ["rick@example.com", "morty@example.com", "jerry@example.com"] @@ -373,6 +375,7 @@ async def test_get_sheets_by_id_hash(async_db_session): @pytest.mark.asyncio async def test_delete_stale_sheets(async_db_session): from datetime import datetime, timedelta + from sqlalchemy.sql import select now = datetime.now() @@ -435,4 +438,4 @@ async def test_delete_stale_sheets(async_db_session): # Running again should not delete anything deleted = await crud.delete_stale_sheets(async_db_session, 7) - assert len(deleted) == 0 \ No newline at end of file + assert len(deleted) == 0 diff --git a/app/tests/web/db/test_user_state.py b/app/tests/web/db/test_user_state.py index 42c61d1..665bf08 100644 --- a/app/tests/web/db/test_user_state.py +++ b/app/tests/web/db/test_user_state.py @@ -1,5 +1,6 @@ from unittest.mock import MagicMock, PropertyMock, patch + import pytest from app.shared.db import models diff --git a/app/tests/web/endpoints/test_default.py b/app/tests/web/endpoints/test_default.py index 401a164..e4e34cc 100644 --- a/app/tests/web/endpoints/test_default.py +++ b/app/tests/web/endpoints/test_default.py @@ -1,10 +1,12 @@ from unittest.mock import MagicMock -from fastapi.testclient import TestClient + import pytest +from fastapi.testclient import TestClient + from app.shared.schemas import Usage, UsageResponse from app.shared.user_groups import GroupInfo -from app.web.config import VERSION from app.tests.web.db.test_crud import test_data +from app.web.config import VERSION def test_endpoint_home(client_with_auth): diff --git a/app/tests/web/endpoints/test_interoperability.py b/app/tests/web/endpoints/test_interoperability.py index 31cf8f0..703f69a 100644 --- a/app/tests/web/endpoints/test_interoperability.py +++ b/app/tests/web/endpoints/test_interoperability.py @@ -1,5 +1,5 @@ -from datetime import datetime import json +from datetime import datetime from unittest.mock import MagicMock, patch from app.shared.db import models diff --git a/app/tests/web/endpoints/test_sheet.py b/app/tests/web/endpoints/test_sheet.py index 1396d85..9b47228 100644 --- a/app/tests/web/endpoints/test_sheet.py +++ b/app/tests/web/endpoints/test_sheet.py @@ -1,5 +1,5 @@ -from datetime import datetime import json +from datetime import datetime from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient @@ -45,8 +45,8 @@ def test_create_sheet_endpoint(app_with_auth, db_session): assert response.json() == {"detail": "User does not have access to this group."} # switch to jerry who's got less quota/permissions - from app.web.security import get_user_state from app.web.db.user_state import UserState + from app.web.security import get_user_state app_with_auth.dependency_overrides[get_user_state] = lambda: UserState(db_session, "jerry@example.com") client_jerry = TestClient(app_with_auth) diff --git a/app/tests/web/test_main.py b/app/tests/web/test_main.py index f77d368..a4ddf1e 100644 --- a/app/tests/web/test_main.py +++ b/app/tests/web/test_main.py @@ -1,10 +1,10 @@ import os -from unittest.mock import patch -from fastapi.testclient import TestClient - import shutil +from unittest.mock import patch import pytest +from fastapi.testclient import TestClient + def test_lifespan(app): with TestClient(app) as client: @@ -25,7 +25,7 @@ def test_logging_middleware(m1, client_with_auth): client_with_auth.delete("/url/123") # creates one empty and one from above assert len(EXCEPTION_COUNTER.collect()[0].samples) == 2 - + def test_serve_local_archive_logic(get_settings): # create a test file first @@ -38,7 +38,7 @@ def test_serve_local_archive_logic(get_settings): get_settings.SERVE_LOCAL_ARCHIVE = "/app/local_archive_test" from app.web.main import app_factory app = app_factory(get_settings) - + # test client = TestClient(app) r = client.get("/app/local_archive_test/temp.txt") diff --git a/app/tests/web/test_security.py b/app/tests/web/test_security.py index 1a6c00b..55a434b 100644 --- a/app/tests/web/test_security.py +++ b/app/tests/web/test_security.py @@ -1,8 +1,8 @@ from unittest.mock import Mock, patch +import pytest from fastapi import HTTPException from fastapi.security import HTTPAuthorizationCredentials -import pytest from app.web.config import ALLOW_ANY_EMAIL @@ -108,8 +108,8 @@ async def test_authenticate_user_exception(): def test_get_user_state(): - from app.web.security import get_user_state from app.web.db.user_state import UserState + from app.web.security import get_user_state mock_session = Mock() test_email = "test@example.com" diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py index d40c457..9a77528 100644 --- a/app/tests/worker/test_worker_main.py +++ b/app/tests/worker/test_worker_main.py @@ -1,13 +1,12 @@ from datetime import datetime - from unittest.mock import patch import pytest - -from app.shared.db import models -from app.shared import schemas from auto_archiver.core import Media, Metadata +from app.shared import schemas +from app.shared.db import models + class Test_create_archive_task(): URL = "https://example-live.com" diff --git a/app/web/__init__.py b/app/web/__init__.py index a817e9e..98a139a 100644 --- a/app/web/__init__.py +++ b/app/web/__init__.py @@ -1,3 +1,4 @@ from app.web.main import app_factory -app = app_factory \ No newline at end of file + +app = app_factory diff --git a/app/web/config.py b/app/web/config.py index 29a6806..b795d88 100644 --- a/app/web/config.py +++ b/app/web/config.py @@ -5,7 +5,7 @@ API_DESCRIPTION = """ **Usage notes:** - The API requires a Bearer token for most operations, which you can obtain by logging in with your Google account. -- You can use this API to archive single URLs or entire Google Sheets. +- You can use this API to archive single URLs or entire Google Sheets. - Once you submit a URL or Sheet for archiving, the API will return a task_id that you can use to check the status of the archiving process. It works asynchronously. """ BREAKING_CHANGES = {"minVersion": "0.4.0", "message": "The latest update has breaking changes, please update the extension to the most recent version."} diff --git a/app/web/db/crud.py b/app/web/db/crud.py index c16b09a..b33faa2 100644 --- a/app/web/db/crud.py +++ b/app/web/db/crud.py @@ -1,18 +1,19 @@ from collections import defaultdict -from functools import lru_cache -from sqlalchemy.orm import Session, load_only -from sqlalchemy import Column, or_, func, select -from loguru import logger from datetime import datetime, timedelta -from sqlalchemy.ext.asyncio import AsyncSession +from functools import lru_cache + from cachetools import LRUCache, cached from cachetools.keys import hashkey +from loguru import logger +from sqlalchemy import Column, func, or_, select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Session, load_only -from app.web.config import ALLOW_ANY_EMAIL from app.shared.db import models from app.shared.settings import get_settings from app.shared.user_groups import UserGroups from app.shared.utils.misc import fnv1a_hash_mod +from app.web.config import ALLOW_ANY_EMAIL from app.web.utils.misc import convert_priority_to_queue_dict @@ -117,7 +118,7 @@ async def get_group_priority_async(db: AsyncSession, group_id: str) -> dict: @cached(cache=LRUCache(maxsize=128), key=lambda db, email: hashkey(email)) def get_user_group_names(db: Session, email: str) -> list[str]: """ - given an email retrieves the user groups from the DB and then the email-domain groups from a global variable, the email does not need to belong to an existing user. + given an email retrieves the user groups from the DB and then the email-domain groups from a global variable, the email does not need to belong to an existing user. """ # TODO: the read: [group1, group2] permissions don't currently work if not email or not len(email) or "@" not in email: return [] @@ -173,7 +174,7 @@ def upsert_user_groups(db: Session): def display_email_pii(email: str): return f"'{email[0:3]}...@{email.split('@')[1]}'" """ - reads the user_groups yaml file and inserts any new users, groups, + reads the user_groups yaml file and inserts any new users, groups, along with new participation of users in groups """ filename = get_settings().USER_GROUPS_FILENAME @@ -192,6 +193,7 @@ def upsert_user_groups(db: Session): for group in explicit_groups: group_domains[group].add(domain) import json + # upsert groups and save a map of groupid -> dbobject for group_id, g in ug.groups.items(): upsert_group(db, group_id, g.description, g.orchestrator, g.orchestrator_sheet, g.service_account_email, json.loads(g.permissions.model_dump_json()), list(group_domains.get(group_id, []))) diff --git a/app/web/db/user_state.py b/app/web/db/user_state.py index 968e1bd..384b0b6 100644 --- a/app/web/db/user_state.py +++ b/app/web/db/user_state.py @@ -1,13 +1,14 @@ -from typing import Dict, Set -import sqlalchemy -from sqlalchemy.orm import Session -from sqlalchemy import func from datetime import datetime +from typing import Dict, Set + +import sqlalchemy +from sqlalchemy import func +from sqlalchemy.orm import Session from app.shared.db import models -from app.shared.user_groups import GroupInfo, GroupPermissions from app.shared.schemas import Usage, UsageResponse +from app.shared.user_groups import GroupInfo, GroupPermissions from app.web.db import crud from app.web.utils.misc import convert_priority_to_queue_dict diff --git a/app/web/endpoints/default.py b/app/web/endpoints/default.py index 9271992..cd23d13 100644 --- a/app/web/endpoints/default.py +++ b/app/web/endpoints/default.py @@ -1,13 +1,15 @@ from typing import Dict + from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import FileResponse, JSONResponse -from app.web.config import VERSION, BREAKING_CHANGES from app.shared.schemas import ActiveUser, UsageResponse +from app.shared.user_groups import GroupInfo +from app.web.config import BREAKING_CHANGES, VERSION from app.web.db.user_state import UserState from app.web.security import get_user_state -from app.shared.user_groups import GroupInfo + default_router = APIRouter() @@ -42,7 +44,7 @@ def get_user_usage( if not user.active: raise HTTPException(status_code=403, detail="User is not active.") return user.usage() - + @default_router.get('/favicon.ico', include_in_schema=False) diff --git a/app/web/endpoints/interoperability.py b/app/web/endpoints/interoperability.py index 06ea175..7892bde 100644 --- a/app/web/endpoints/interoperability.py +++ b/app/web/endpoints/interoperability.py @@ -1,19 +1,19 @@ import json + +import sqlalchemy +from auto_archiver.core import Metadata from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import JSONResponse from loguru import logger -import sqlalchemy -from auto_archiver.core import Metadata from sqlalchemy.orm import Session -from app.shared.aa_utils import get_all_urls -from app.web.config import ALLOW_ANY_EMAIL from app.shared import business_logic, schemas -from app.shared.db import worker_crud +from app.shared.aa_utils import get_all_urls +from app.shared.db import models, worker_crud from app.shared.db.database import get_db_dependency -from app.web.security import token_api_key_auth -from app.shared.db import models from app.shared.log import log_error +from app.web.config import ALLOW_ANY_EMAIL +from app.web.security import token_api_key_auth interoperability_router = APIRouter(prefix="/interop", tags=["Interoperability endpoints."]) diff --git a/app/web/endpoints/sheet.py b/app/web/endpoints/sheet.py index 7848b5e..d8c089a 100644 --- a/app/web/endpoints/sheet.py +++ b/app/web/endpoints/sheet.py @@ -1,16 +1,16 @@ from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import JSONResponse - from sqlalchemy import exc from sqlalchemy.orm import Session -from app.web.db.user_state import UserState from app.shared import schemas -from app.shared.task_messaging import get_celery -from app.web.security import get_user_state -from app.web.db import crud from app.shared.db.database import get_db_dependency +from app.shared.task_messaging import get_celery +from app.web.db import crud +from app.web.db.user_state import UserState +from app.web.security import get_user_state + sheet_router = APIRouter(prefix="/sheet", tags=["Google Spreadsheet operations"]) @@ -78,4 +78,4 @@ def archive_user_sheet( group_queue = user.priority_group(sheet.group_id) task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=id, author_id=user.email, group_id=sheet.group_id).model_dump_json()]).apply_async(**group_queue) - return JSONResponse({"id": task.id}, status_code=201) \ No newline at end of file + return JSONResponse({"id": task.id}, status_code=201) diff --git a/app/web/endpoints/task.py b/app/web/endpoints/task.py index 610c579..3f2ff94 100644 --- a/app/web/endpoints/task.py +++ b/app/web/endpoints/task.py @@ -3,10 +3,10 @@ from fastapi import APIRouter, Depends from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse -from app.shared.task_messaging import get_celery -from app.web.security import get_token_or_user_auth from app.shared import schemas from app.shared.log import log_error +from app.shared.task_messaging import get_celery +from app.web.security import get_token_or_user_auth from app.web.utils.misc import custom_jsonable_encoder diff --git a/app/web/endpoints/url.py b/app/web/endpoints/url.py index a7ac4b4..8307c2d 100644 --- a/app/web/endpoints/url.py +++ b/app/web/endpoints/url.py @@ -1,22 +1,22 @@ +from datetime import datetime +from urllib.parse import urlparse + from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import JSONResponse -from datetime import datetime from loguru import logger from sqlalchemy.orm import Session -from app.web.config import ALLOW_ANY_EMAIL from app.shared import schemas +from app.shared.db.database import get_db_dependency from app.shared.task_messaging import get_celery -from app.web.security import get_token_or_user_auth, get_user_state +from app.web.config import ALLOW_ANY_EMAIL from app.web.db import crud from app.web.db.user_state import UserState -from app.shared.db.database import get_db_dependency - -from urllib.parse import urlparse - +from app.web.security import get_token_or_user_auth, get_user_state from app.web.utils.misc import convert_priority_to_queue_dict + url_router = APIRouter(prefix="/url", tags=["Single URL operations"]) celery = get_celery() @@ -47,7 +47,7 @@ def archive_url( else: archive_create.author_id = archive.author_id or email group_queue = convert_priority_to_queue_dict("high") - + task = celery.signature("create_archive_task", args=[archive_create.model_dump_json()]).apply_async(**group_queue) task_response = schemas.Task(id=task.id) @@ -74,8 +74,8 @@ def search_by_url( @url_router.delete("/{id}", summary="Delete a single URL archive by id.") def delete_archive( - id:str, - user: UserState = Depends(get_user_state), + id:str, + user: UserState = Depends(get_user_state), db: Session = Depends(get_db_dependency) ) -> schemas.DeleteResponse: logger.info(f"deleting url archive task {id} request by {user.email}") diff --git a/app/web/events.py b/app/web/events.py index 625731a..fa15614 100644 --- a/app/web/events.py +++ b/app/web/events.py @@ -1,22 +1,32 @@ import asyncio -from collections import defaultdict import datetime import logging +from collections import defaultdict +from contextlib import asynccontextmanager + import alembic.config from fastapi import FastAPI -from contextlib import asynccontextmanager +from fastapi_mail import FastMail, MessageSchema, MessageType from fastapi_utils.tasks import repeat_every from loguru import logger -from fastapi_mail import FastMail, MessageSchema, MessageType -from app.shared.db import models -from app.shared.db.database import get_db, get_db_async, make_engine, wal_checkpoint from app.shared import schemas +from app.shared.db import models +from app.shared.db.database import ( + get_db, + get_db_async, + make_engine, + wal_checkpoint, +) from app.shared.settings import get_settings from app.shared.task_messaging import get_celery from app.web.db import crud from app.web.middleware import increase_exceptions_counter -from app.web.utils.metrics import measure_regular_metrics, redis_subscribe_worker_exceptions +from app.web.utils.metrics import ( + measure_regular_metrics, + redis_subscribe_worker_exceptions, +) + celery = get_celery() @@ -183,4 +193,4 @@ async def delete_stale_sheets(): async def generate_users_export_csv(): #TODO: implement a cronjob that regularly requested user data to a CSV file # see https://colab.research.google.com/drive/1QDbo3QXHPBdiTuANlA1AWVvN-rqxuCPa?authuser=0#scrollTo=4nPXeSdK8RBT - pass \ No newline at end of file + pass diff --git a/app/web/main.py b/app/web/main.py index ff2266e..69af5c6 100644 --- a/app/web/main.py +++ b/app/web/main.py @@ -1,24 +1,23 @@ import os -from fastapi import FastAPI, Depends -from fastapi.staticfiles import StaticFiles + +from fastapi import Depends, FastAPI from fastapi.middleware.cors import CORSMiddleware -from prometheus_fastapi_instrumentator import Instrumentator +from fastapi.staticfiles import StaticFiles from loguru import logger +from prometheus_fastapi_instrumentator import Instrumentator -from app.web.middleware import logging_middleware -from app.shared.task_messaging import get_celery - -from app.web.security import token_api_key_auth -from app.web.config import VERSION, API_DESCRIPTION -from app.web.events import lifespan from app.shared.settings import get_settings - - +from app.shared.task_messaging import get_celery +from app.web.config import API_DESCRIPTION, VERSION from app.web.endpoints.default import default_router -from app.web.endpoints.url import url_router +from app.web.endpoints.interoperability import interoperability_router from app.web.endpoints.sheet import sheet_router from app.web.endpoints.task import task_router -from app.web.endpoints.interoperability import interoperability_router +from app.web.endpoints.url import url_router +from app.web.events import lifespan +from app.web.middleware import logging_middleware +from app.web.security import token_api_key_auth + celery = get_celery() @@ -57,4 +56,4 @@ def app_factory(settings = get_settings()): logger.warning(f"MOUNTing local archive, use this in development only {settings.SERVE_LOCAL_ARCHIVE}") app.mount(settings.SERVE_LOCAL_ARCHIVE, StaticFiles(directory=local_dir), name=settings.SERVE_LOCAL_ARCHIVE) - return app \ No newline at end of file + return app diff --git a/app/web/middleware.py b/app/web/middleware.py index 52da626..5ddca4b 100644 --- a/app/web/middleware.py +++ b/app/web/middleware.py @@ -1,7 +1,9 @@ import traceback -from loguru import logger + from fastapi import Request +from loguru import logger + from app.shared.log import log_error from app.web.utils.metrics import EXCEPTION_COUNTER @@ -25,7 +27,7 @@ async def increase_exceptions_counter(e: Exception, location:str="cronjob"): last_trace = traceback.extract_tb(e.__traceback__)[-1] _file, _line, func_name, _text = last_trace location = func_name - except Exception as e: + except Exception as e: logger.error(f"Unable to get function name from cronjob exception traceback: {e}") EXCEPTION_COUNTER.labels(type=e.__class__.__name__, location=location).inc() - log_error(e) \ No newline at end of file + log_error(e) diff --git a/app/web/security.py b/app/web/security.py index 12115af..494e094 100644 --- a/app/web/security.py +++ b/app/web/security.py @@ -1,14 +1,17 @@ +import secrets + +import requests +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from loguru import logger -import requests, secrets -from fastapi import HTTPException, status, Depends -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.orm import Session -from app.web.config import ALLOW_ANY_EMAIL -from app.shared.settings import get_settings from app.shared.db.database import get_db_dependency +from app.shared.settings import get_settings +from app.web.config import ALLOW_ANY_EMAIL from app.web.db.user_state import UserState + settings = get_settings() bearer_security = HTTPBearer() @@ -80,4 +83,4 @@ def authenticate_user(access_token): def get_user_state(email:str=Depends(get_user_auth), db:Session=Depends(get_db_dependency)): - return UserState(db, email) \ No newline at end of file + return UserState(db, email) diff --git a/app/web/utils/metrics.py b/app/web/utils/metrics.py index a885b9a..d8026a1 100644 --- a/app/web/utils/metrics.py +++ b/app/web/utils/metrics.py @@ -2,12 +2,13 @@ import asyncio import json import os import shutil + from prometheus_client import Counter, Gauge -from app.web.db import crud from app.shared.db.database import get_db from app.shared.log import log_error from app.shared.task_messaging import get_redis +from app.web.db import crud # Custom metrics diff --git a/app/web/utils/misc.py b/app/web/utils/misc.py index 870a60b..16a6591 100644 --- a/app/web/utils/misc.py +++ b/app/web/utils/misc.py @@ -1,4 +1,5 @@ import base64 + from fastapi.encoders import jsonable_encoder diff --git a/app/worker/main.py b/app/worker/main.py index 561245b..7b4826b 100644 --- a/app/worker/main.py +++ b/app/worker/main.py @@ -1,21 +1,22 @@ +import datetime import json +import traceback -import traceback, datetime +from auto_archiver.core.orchestrator import ArchivingOrchestrator from celery.signals import task_failure from loguru import logger from sqlalchemy import exc -from auto_archiver.core.orchestrator import ArchivingOrchestrator -from app.shared.db import models -from app.shared.db.database import get_db from app.shared import business_logic, schemas -from app.shared.task_messaging import get_celery, get_redis -from app.shared.settings import get_settings -from app.shared.log import log_error from app.shared.aa_utils import get_all_urls -from app.shared.db import worker_crud +from app.shared.db import models, worker_crud +from app.shared.db.database import get_db +from app.shared.log import log_error +from app.shared.settings import get_settings +from app.shared.task_messaging import get_celery, get_redis from app.worker.worker_log import setup_celery_logger + settings = get_settings() celery = get_celery("worker") @@ -26,7 +27,7 @@ USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME setup_celery_logger(celery) # TODO: these are temporary PATCHES for new aa's functionality -# logger.add("app/worker/worker_log.log", level="DEBUG") +# logger.add("app/worker/worker_log.log", level="DEBUG") logger.remove = lambda x: print(f"logger.remove({x})") # TODO: after release, as it requires updating past entries with sheet_id where tag is used, drop tags diff --git a/app/worker/worker_log.py b/app/worker/worker_log.py index 221d824..022c63d 100644 --- a/app/worker/worker_log.py +++ b/app/worker/worker_log.py @@ -1,9 +1,11 @@ -from loguru import logger -from celery import Celery import sys +from celery import Celery +from loguru import logger + from app.shared.task_messaging import get_celery + celery = get_celery("worker") def setup_celery_logger(celery): @@ -22,7 +24,7 @@ def setup_celery_logger(celery): if message.strip(): logger.info(message.strip()) # Required to prevent issues with buffered output - def flush(self): pass + def flush(self): pass def isatty(self): return False sys.stdout = InterceptHandler() diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 29088c0..9c88a36 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -12,7 +12,7 @@ services: - ALLOWED_ORIGINS=["http://localhost:8000","http://localhost:8004","http://localhost:8081","chrome-extension://ojcimmjndnlmmlgnjaeojoebaceokpdp"] - USER_GROUPS_FILENAME=/aa-api/app/user-groups.dev.yaml - DATABASE_PATH=sqlite:////aa-api/database/auto-archiver.db - + worker: # command: watchmedo auto-restart --patterns="*.py" --recursive --ignore-directories -- celery -- --app=app.worker.main.celery worker -Q high_priority,low_priority --concurrency=${CONCURRENCY} --max-tasks-per-child=100 diff --git a/docker-compose.yml b/docker-compose.yml index 31969bc..737c346 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ volumes: name: "auto-archiver-api" services: web: - build: + build: context: . dockerfile: web.Dockerfile restart: always @@ -29,7 +29,7 @@ services: retries: 3 worker: - build: + build: context: . dockerfile: worker.Dockerfile restart: always @@ -68,4 +68,4 @@ services: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 30s timeout: 10s - retries: 3 \ No newline at end of file + retries: 3 diff --git a/poetry.lock b/poetry.lock index ceb824a..9c1ea55 100644 --- a/poetry.lock +++ b/poetry.lock @@ -616,6 +616,18 @@ files = [ [package.dependencies] pycparser = "*" +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "charset-normalizer" version = "3.4.1" @@ -959,6 +971,18 @@ calendars = ["convertdate (>=2.2.1)", "hijridate"] fasttext = ["fasttext (>=0.9.1)", "numpy (>=1.19.3,<2)"] langdetect = ["langdetect (>=1.0.0)"] +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + [[package]] name = "dnspython" version = "2.7.0" @@ -1097,6 +1121,23 @@ future = "*" [package.extras] dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4.6.1)", "pytest-mock (==1.10.4)", "tox (==3.12.1)"] +[[package]] +name = "filelock" +version = "3.17.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, + {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] + [[package]] name = "future" version = "1.0.0" @@ -1409,6 +1450,21 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "identify" +version = "2.6.8" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.8-py2.py3-none-any.whl", hash = "sha256:83657f0f766a3c8d0eaea16d4ef42494b39b34629a4b3192a9d020d349b3e255"}, + {file = "identify-2.6.8.tar.gz", hash = "sha256:61491417ea2c0c5c670484fd8abbb34de34cdae1e5f39a73ee65e48e4bb663fc"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.10" @@ -1724,6 +1780,18 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + [[package]] name = "numpy" version = "2.1.3" @@ -1981,6 +2049,23 @@ tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "ole typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] +[[package]] +name = "platformdirs" +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + [[package]] name = "pluggy" version = "1.5.0" @@ -1997,6 +2082,25 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pre-commit" +version = "4.1.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, + {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "prometheus-client" version = "0.21.1" @@ -2557,7 +2661,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["web"] +groups = ["dev", "web"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -3415,6 +3519,27 @@ files = [ {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, ] +[[package]] +name = "virtualenv" +version = "20.29.2" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, + {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + [[package]] name = "vk-api" version = "11.9.9" @@ -3688,4 +3813,4 @@ test = ["pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "11d734f2ee32206214a7ecb8dc3ec8d19a7b6281ee98b509a5bb8bdb647c674a" +content-hash = "c4a5c50ac109c9912992ca86d2b5ec712c6bcfc84838bf42f90208b02cc27b3c" diff --git a/pyproject.toml b/pyproject.toml index ea1c87d..df7b789 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,4 +52,4 @@ pytest = ">=8.3.4,<9.0.0" httpx = ">=0.28.1,<0.29.0" coverage = ">=7.6.11,<8.0.0" pytest-asyncio = ">=0.25.3,<0.26.0" - +pre-commit = "^4.1.0" diff --git a/user-groups.example.yaml b/user-groups.example.yaml index 32f99b4..22a919d 100644 --- a/user-groups.example.yaml +++ b/user-groups.example.yaml @@ -59,4 +59,3 @@ groups: permissions: read: ["default"] read_public: true - \ No newline at end of file diff --git a/worker.Dockerfile b/worker.Dockerfile index 4e24f87..9154c32 100644 --- a/worker.Dockerfile +++ b/worker.Dockerfile @@ -30,4 +30,4 @@ COPY alembic.ini ./ COPY ./app/ ./app/ COPY user-groups.* ./app/ -ENTRYPOINT ["./poetry-venv/bin/poetry", "run"] \ No newline at end of file +ENTRYPOINT ["./poetry-venv/bin/poetry", "run"] From 229db7dd5c88a2af886bbd5cc701f80b865f78f2 Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Wed, 26 Feb 2025 14:37:49 -0600 Subject: [PATCH 08/40] =?UTF-8?q?Add=20test=20CI=20Action=20with=20multipl?= =?UTF-8?q?e=20=F0=9F=90=8D=20versions=20(#57)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- .github/workflows/format-and-fail.yml | 2 +- .github/workflows/test.yml | 40 +++++++++++++++++++++++++++ app/__init__.py | 0 pytest.ini | 2 ++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 app/__init__.py create mode 100644 pytest.ini diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4135ac2..6b8c556 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: branches: [ main, dev ] jobs: - test: + test-with-coverage: runs-on: ubuntu-latest services: diff --git a/.github/workflows/format-and-fail.yml b/.github/workflows/format-and-fail.yml index f1c01f8..c203aa9 100644 --- a/.github/workflows/format-and-fail.yml +++ b/.github/workflows/format-and-fail.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.11" - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1ad37f3 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: Run Tests +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.10', '3.11', '3.12'] + + services: + redis: + image: redis:6-alpine + ports: + - 6379:6379 + + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Poetry + run: pipx install poetry + + - name: Install dependencies + run: poetry install --no-interaction --with dev + + - name: Set dev environment variable + run: echo "ENVIRONMENT_FILE=.env.test" >> $GITHUB_ENV + + - name: Run tests + run: poetry run pytest app/tests diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = . From d575b6f9af5e72ad2f6042dbea5161603f61f756 Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Thu, 27 Feb 2025 12:35:23 -0600 Subject: [PATCH 09/40] Format and lint the `tests` directory (#58) --- .pre-commit-config.yaml | 7 +- app/tests/conftest.py | 148 +++- app/tests/shared/db/test_models.py | 11 +- app/tests/shared/db/test_worker_crud.py | 51 +- app/tests/shared/test_business_logic.py | 14 +- app/tests/web/db/test_crud.py | 714 ++++++++++++++---- app/tests/web/db/test_user_state.py | 613 ++++++++++++--- app/tests/web/endpoints/test_default.py | 80 +- .../web/endpoints/test_interoperability.py | 131 +++- app/tests/web/endpoints/test_sheet.py | 202 +++-- app/tests/web/endpoints/test_task.py | 14 +- app/tests/web/endpoints/test_url.py | 223 ++++-- app/tests/web/test_main.py | 24 +- app/tests/web/test_security.py | 150 ++-- app/tests/worker/test_worker_main.py | 97 ++- 15 files changed, 1894 insertions(+), 585 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 73765d5..5e1d2a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: - id: nbqa-ruff args: - --fix - - --target-version=py311 + - --target-version=py310 - --ignore=E721,E722 - --line-length=80 - id: nbqa-black @@ -62,18 +62,17 @@ repos: - --line-length=80 # - repo: https://github.com/astral-sh/ruff-pre-commit -# rev: v0.4.10 +# rev: v0.9.7 # hooks: # - id: ruff # types_or: [python,pyi] # args: # - --fix -# - --target-version=py311 # - --select=B,C,E,F,W,B9 # - --line-length=80 # - --ignore=E203,E402,E501,E261 # - id: ruff-format # types_or: [ python,pyi] # args: -# - --target-version=py311 +# - --target-version=py310 # - --line-length=80 diff --git a/app/tests/conftest.py b/app/tests/conftest.py index f7da39e..997ea62 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -1,4 +1,6 @@ import os +from datetime import datetime +from http import HTTPStatus from typing import AsyncGenerator from unittest.mock import patch @@ -7,15 +9,31 @@ import pytest_asyncio from fastapi.testclient import TestClient from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession +from app.shared.db import models +from app.shared.db.database import ( + make_async_engine, + make_async_session_local, + make_engine, + make_session_local, +) from app.shared.settings import Settings from app.web.config import ALLOW_ANY_EMAIL +from app.web.db import crud +from app.web.db.crud import get_user_group_names from app.web.db.user_state import UserState +from app.web.main import app_factory +from app.web.security import ( + get_token_or_user_auth, + get_user_auth, + get_user_state, + token_api_key_auth, +) @pytest.fixture(autouse=True) def mock_logger_add(): """Fixture to mock loguru.logger.add for all tests.""" - with patch('loguru.logger.add') as mock_add: + with patch("loguru.logger.add") as mock_add: yield mock_add # This makes the mock available to tests @@ -26,23 +44,22 @@ def get_settings(): @pytest.fixture(autouse=True) def mock_settings(): - with patch('app.shared.settings.Settings', return_value=Settings(_env_file=".env.test")) as mock_settings: + with patch( + "app.shared.settings.Settings", + return_value=Settings(_env_file=".env.test"), + ) as mock_settings: yield mock_settings @pytest.fixture() def test_db(get_settings: Settings): - from app.shared.db import models - from app.shared.db.database import make_engine - from app.web.db.crud import get_user_group_names - get_user_group_names.cache_clear() make_engine.cache_clear() engine = make_engine(get_settings.DATABASE_PATH) fs = get_settings.DATABASE_PATH.replace("sqlite:///", "") if not os.path.exists(fs): - open(fs, 'w').close() + open(fs, "w").close() models.Base.metadata.create_all(engine) @@ -59,7 +76,6 @@ def test_db(get_settings: Settings): @pytest.fixture() def db_session(test_db): - from app.shared.db.database import make_session_local session_local = make_session_local(test_db) with session_local() as session: yield session @@ -67,18 +83,12 @@ def db_session(test_db): @pytest_asyncio.fixture() async def async_test_db(get_settings: Settings): - import asyncio - - from app.shared.db import models - from app.shared.db.database import make_async_engine - from app.web.db.crud import get_user_group_names - get_user_group_names.cache_clear() engine = await make_async_engine(get_settings.ASYNC_DATABASE_PATH) fs = get_settings.ASYNC_DATABASE_PATH.replace("sqlite+aiosqlite:///", "") if not os.path.exists(fs): - open(fs, 'w').close() + open(fs, "w").close() async def create_all(): async with engine.begin() as conn: @@ -102,8 +112,9 @@ async def async_test_db(get_settings: Settings): @pytest_asyncio.fixture() -async def async_db_session(async_test_db: AsyncEngine) -> AsyncGenerator[AsyncSession, None]: - from app.shared.db.database import make_async_session_local +async def async_db_session( + async_test_db: AsyncEngine, +) -> AsyncGenerator[AsyncSession, None]: session_local = await make_async_session_local(async_test_db) async with session_local() as session: yield session @@ -111,8 +122,6 @@ async def async_db_session(async_test_db: AsyncEngine) -> AsyncGenerator[AsyncSe @pytest.fixture() def app(db_session): - from app.web.db import crud - from app.web.main import app_factory app = app_factory() crud.upsert_user_groups(db_session) return app @@ -126,14 +135,13 @@ def client(app): @pytest.fixture() def app_with_auth(app, db_session): - from app.web.security import ( - get_token_or_user_auth, - get_user_auth, - get_user_state, + app.dependency_overrides[get_token_or_user_auth] = ( + lambda: "rick@example.com" ) - app.dependency_overrides[get_token_or_user_auth] = lambda: "rick@example.com" app.dependency_overrides[get_user_auth] = lambda: "morty@example.com" - app.dependency_overrides[get_user_state] = lambda: UserState(db_session, "MORTY@example.com") + app.dependency_overrides[get_user_state] = lambda: UserState( + db_session, "MORTY@example.com" + ) return app @@ -145,7 +153,6 @@ def client_with_auth(app_with_auth): @pytest.fixture() def app_with_token(app): - from app.web.security import get_token_or_user_auth, token_api_key_auth app.dependency_overrides[token_api_key_auth] = lambda: ALLOW_ANY_EMAIL app.dependency_overrides[get_token_or_user_auth] = lambda: ALLOW_ANY_EMAIL return app @@ -162,6 +169,93 @@ def test_no_auth(): # reusable code to ensure a method/endpoint combination is unauthorized def no_auth(http_method, endpoint): response = http_method(endpoint) - assert response.status_code == 403 + assert response.status_code == HTTPStatus.FORBIDDEN assert response.json() == {"detail": "Not authenticated"} + return no_auth + + +@pytest.fixture() +def test_data(db_session): + author_emails = [ + "rick@example.com", + "morty@example.com", + "jerry@example.com", + ] + + # creates 3 users + for email in author_emails: + db_session.add(models.User(email=email)) + db_session.commit() + assert db_session.query(models.User).count() == 3 + + # creates 100 archives for 3 users over 2 months with repeating URLs + for i in range(100): + author = author_emails[i % 3] + archive = models.Archive( + id=f"archive-id-456-{i}", + url=f"https://example-{i % 3}.com", + result={}, + public=author == "jerry@example.com", + author_id=author, + group_id="spaceship" + if author == "morty@example.com" and i % 2 == 0 + else None, + created_at=datetime(2021, (i % 2) + 1, (i % 25) + 1), + ) + if i % 5 == 0: + archive.tags.append(models.Tag(id=f"tag-{i}")) + if i % 10 == 0: + archive.tags.append(models.Tag(id=f"tag-second-{i}")) + if i % 4 == 0: + archive.tags.append(models.Tag(id=f"tag-third-{i}")) + for j in range(10): + archive.urls.append( + models.ArchiveUrl( + url=f"https://example-{i}.com/{j}", key=f"media_{j}" + ) + ) + db_session.add(archive) + + # creates a sheet for each user + for i, email in enumerate( + ["rick@example.com", "morty@example.com", "jerry@example.com"] + ): + db_session.add( + models.Sheet( + id=f"sheet-{i}", + name=f"sheet-{i}", + author_id=email, + group_id=None, + frequency="daily", + ) + ) + if email == "rick@example.com": + db_session.add( + models.Sheet( + id=f"sheet-{i}-2", + name=f"sheet-{i}-2", + author_id=email, + group_id="spaceship", + frequency="hourly", + ) + ) + + db_session.commit() + + assert db_session.query(models.Archive).count() == 100 + assert db_session.query(models.Tag).count() == 20 + 10 + 25 + assert db_session.query(models.ArchiveUrl).count() == 1000 + assert ( + db_session.query(models.ArchiveUrl) + .filter(models.ArchiveUrl.archive_id == "archive-id-456-0") + .count() + == 10 + ) + + # setup groups + assert db_session.query(models.Group).count() == 0 + + crud.upsert_user_groups(db_session) + assert db_session.query(models.Group).count() == 4 + assert db_session.query(models.User).count() == 3 diff --git a/app/tests/shared/db/test_models.py b/app/tests/shared/db/test_models.py index 4da9571..537532b 100644 --- a/app/tests/shared/db/test_models.py +++ b/app/tests/shared/db/test_models.py @@ -1,6 +1,7 @@ -def test_generate_uuid(): - from app.shared.db.models import generate_uuid +from app.shared.db.models import generate_uuid - assert generate_uuid() != generate_uuid() - assert len(generate_uuid()) == 36 - assert generate_uuid().count("-") == 4 + +def test_generate_uuid(): + assert generate_uuid() != generate_uuid() + assert len(generate_uuid()) == 36 + assert generate_uuid().count("-") == 4 diff --git a/app/tests/shared/db/test_worker_crud.py b/app/tests/shared/db/test_worker_crud.py index 09e9a76..2258781 100644 --- a/app/tests/shared/db/test_worker_crud.py +++ b/app/tests/shared/db/test_worker_crud.py @@ -1,11 +1,10 @@ from datetime import datetime +from app.shared import schemas from app.shared.db import models, worker_crud -from app.tests.web.db.test_crud import test_data def test_update_sheet_last_url_archived_at(db_session): - # Create test sheet test_sheet = models.Sheet(id="sheet-123") db_session.add(test_sheet) @@ -14,17 +13,24 @@ def test_update_sheet_last_url_archived_at(db_session): # Test updating existing sheet assert isinstance(test_sheet.last_url_archived_at, datetime) before = test_sheet.last_url_archived_at - assert worker_crud.update_sheet_last_url_archived_at(db_session, "sheet-123") is True + assert ( + worker_crud.update_sheet_last_url_archived_at(db_session, "sheet-123") + is True + ) db_session.refresh(test_sheet) assert isinstance(test_sheet.last_url_archived_at, datetime) assert test_sheet.last_url_archived_at > before # Test non-existent sheet - assert worker_crud.update_sheet_last_url_archived_at(db_session, "non-existent-sheet") is False + assert ( + worker_crud.update_sheet_last_url_archived_at( + db_session, "non-existent-sheet" + ) + is False + ) + def test_get_group(test_data, db_session): - from app.shared.db import worker_crud - assert worker_crud.get_group(db_session, "spaceship") is not None assert worker_crud.get_group(db_session, "interdimensional") is not None assert worker_crud.get_group(db_session, "animated-characters") is not None @@ -32,24 +38,24 @@ def test_get_group(test_data, db_session): def test_create_or_get_user(test_data, db_session): - from app.shared.db import worker_crud - assert db_session.query(models.User).count() == 3 # already exists - assert (u1 := worker_crud.create_or_get_user(db_session, "rick@example.com")) is not None + assert ( + u1 := worker_crud.create_or_get_user(db_session, "rick@example.com") + ) is not None assert u1.email == "rick@example.com" # new user - assert (u2 := worker_crud.create_or_get_user(db_session, "beth@example.com")) is not None + assert ( + u2 := worker_crud.create_or_get_user(db_session, "beth@example.com") + ) is not None assert u2.email == "beth@example.com" assert db_session.query(models.User).count() == 4 def test_create_tag(db_session): - from app.shared.db import worker_crud - assert db_session.query(models.Tag).count() == 0 # create first @@ -57,7 +63,10 @@ def test_create_tag(db_session): assert create_tag is not None assert create_tag.id == "tag-101" assert db_session.query(models.Tag).count() == 1 - assert db_session.query(models.Tag).filter(models.Tag.id == "tag-101").first() == create_tag + assert ( + db_session.query(models.Tag).filter(models.Tag.id == "tag-101").first() + == create_tag + ) # same id does not add new db entry existing_tag = worker_crud.create_tag(db_session, "tag-101") @@ -72,9 +81,6 @@ def test_create_tag(db_session): def test_create_task(db_session): - from app.shared import schemas - from app.shared.db import worker_crud - task = schemas.ArchiveCreate( id="archive-id-456-101", url="https://example-0.com", @@ -83,17 +89,22 @@ def test_create_task(db_session): author_id="rick@example.com", group_id="spaceship", tags=[], - urls=[] + urls=[], ) # with tags and urls - nt = worker_crud.create_archive(db_session, task, [models.Tag(id="tag-101")], [models.ArchiveUrl(url="https://example-0.com/0", key="media_0")]) + nt = worker_crud.create_archive( + db_session, + task, + [models.Tag(id="tag-101")], + [models.ArchiveUrl(url="https://example-0.com/0", key="media_0")], + ) assert nt is not None assert nt.id == "archive-id-456-101" assert nt.url == "https://example-0.com" assert nt.author_id == "rick@example.com" - assert nt.public == False + assert nt.public is False assert nt.group_id == "spaceship" assert len(nt.tags) == 1 assert nt.tags[0].id == "tag-101" @@ -109,7 +120,7 @@ def test_create_task(db_session): assert nt.id == "archive-id-456-102" assert nt.url == "https://example-0.com" assert nt.author_id == "rick@example.com" - assert nt.public == False + assert nt.public is False assert nt.group_id == "spaceship" assert len(nt.tags) == 0 assert len(nt.urls) == 0 diff --git a/app/tests/shared/test_business_logic.py b/app/tests/shared/test_business_logic.py index 830fa7b..e10d402 100644 --- a/app/tests/shared/test_business_logic.py +++ b/app/tests/shared/test_business_logic.py @@ -9,7 +9,7 @@ from app.shared.business_logic import ( ) -class Test_get_store_archive_until: +class TestGetStoreArchiveUntil: GROUP_ID = "test-group" def test_group_not_found(self, db_session): @@ -17,7 +17,10 @@ class Test_get_store_archive_until: get_store_archive_until(db_session, self.GROUP_ID) assert str(exc.value) == f"Group {self.GROUP_ID} not found." - @patch("app.shared.db.worker_crud.get_group", return_value=MagicMock(permissions=None)) + @patch( + "app.shared.db.worker_crud.get_group", + return_value=MagicMock(permissions=None), + ) def test_group_no_permissions(self, db_session): with pytest.raises(AssertionError) as exc: get_store_archive_until(db_session, self.GROUP_ID) @@ -48,14 +51,17 @@ class Test_get_store_archive_until: mock_get_group.assert_called_once_with(db_session, self.GROUP_ID) -class Test_get_store_archive_until_or_never: +class TestGetStoreArchiveUntilOrNever: GROUP_ID = "test-group" def test_group_not_found(self, db_session): result = get_store_archive_until_or_never(db_session, self.GROUP_ID) assert result is None - @patch("app.shared.db.worker_crud.get_group", return_value=MagicMock(permissions=None)) + @patch( + "app.shared.db.worker_crud.get_group", + return_value=MagicMock(permissions=None), + ) def test_group_no_permissions(self, db_session): result = get_store_archive_until_or_never(db_session, self.GROUP_ID) assert result is None diff --git a/app/tests/web/db/test_crud.py b/app/tests/web/db/test_crud.py index 298a087..676aa24 100644 --- a/app/tests/web/db/test_crud.py +++ b/app/tests/web/db/test_crud.py @@ -2,125 +2,379 @@ from datetime import datetime, timedelta from unittest.mock import patch import pytest +import sqlalchemy import yaml +from sqlalchemy import false, true +from sqlalchemy.sql import select from app.shared.db import models from app.shared.settings import Settings +from app.web.config import ALLOW_ANY_EMAIL from app.web.db import crud -authors = ["rick@example.com", "morty@example.com", "jerry@example.com"] - - -@pytest.fixture() -def test_data(db_session): - - # creates 3 users - for email in authors: - db_session.add(models.User(email=email)) - db_session.commit() - assert db_session.query(models.User).count() == 3 - - # creates 100 archives for 3 users over 2 months with repeating URLs - for i in range(100): - author = authors[i % 3] - archive = models.Archive( - id=f"archive-id-456-{i}", - url=f"https://example-{i%3}.com", - result={}, - public=author == "jerry@example.com", - author_id=author, - group_id="spaceship" if author == "morty@example.com" and i % 2 == 0 else None, - created_at=datetime(2021, (i % 2) + 1, (i % 25) + 1) - ) - if i % 5 == 0: - archive.tags.append(models.Tag(id=f"tag-{i}")) - if i % 10 == 0: - archive.tags.append(models.Tag(id=f"tag-second-{i}")) - if i % 4 == 0: - archive.tags.append(models.Tag(id=f"tag-third-{i}")) - for j in range(10): - archive.urls.append(models.ArchiveUrl(url=f"https://example-{i}.com/{j}", key=f"media_{j}")) - db_session.add(archive) - - # creates a sheet for each user - for i, email in enumerate(authors): - db_session.add(models.Sheet(id=f"sheet-{i}", name=f"sheet-{i}", author_id=email, group_id=None, frequency="daily")) - if email == "rick@example.com": - db_session.add(models.Sheet(id=f"sheet-{i}-2", name=f"sheet-{i}-2", author_id=email, group_id="spaceship", frequency="hourly")) - - db_session.commit() - - assert db_session.query(models.Archive).count() == 100 - assert db_session.query(models.Tag).count() == 20 + 10 + 25 - assert db_session.query(models.ArchiveUrl).count() == 1000 - assert db_session.query(models.ArchiveUrl).filter(models.ArchiveUrl.archive_id == "archive-id-456-0").count() == 10 - - # setup groups - assert db_session.query(models.Group).count() == 0 - from app.web.db import crud - crud.upsert_user_groups(db_session) - assert db_session.query(models.Group).count() == 4 - assert db_session.query(models.User).count() == 3 - - def test_search_archives_by_url(test_data, db_session): - from app.web.config import ALLOW_ANY_EMAIL - - # rick's archives are private - assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "rick@example.com", True, False)) == 34 - assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "rick@example.com", [], False)) == 34 - assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "rick@example.com", [], True)) == 34 - assert len(crud.search_archives_by_url(db_session, "https://example-0.com", ALLOW_ANY_EMAIL, [], False)) == 34 - assert len(crud.search_archives_by_url(db_session, "https://example-0.com", ALLOW_ANY_EMAIL, True, False)) == 34 - assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "morty@example.com", [], False)) == 0 - assert len(crud.search_archives_by_url(db_session, "https://example-0.com", "morty@example.com", [], True)) == 0 + # Rick's archives are private + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-0.com", + "rick@example.com", + True, + False, + ) + ) + == 34 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-0.com", + "rick@example.com", + [], + False, + ) + ) + == 34 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-0.com", + "rick@example.com", + [], + True, + ) + ) + == 34 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, "https://example-0.com", ALLOW_ANY_EMAIL, [], False + ) + ) + == 34 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-0.com", + ALLOW_ANY_EMAIL, + True, + False, + ) + ) + == 34 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-0.com", + "morty@example.com", + [], + False, + ) + ) + == 0 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-0.com", + "morty@example.com", + [], + True, + ) + ) + == 0 + ) # morty's archives are public but half are in spaceship group - assert len(crud.search_archives_by_url(db_session, "https://example-1.com", "rick@example.com", ["spaceship"], False)) == 16 - assert len(crud.search_archives_by_url(db_session, "https://example-1.com", "rick@example.com", True, False)) == 16 - assert len(crud.search_archives_by_url(db_session, "https://example-1.com", "jerry@example.com", True, True)) == 16 + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-1.com", + "rick@example.com", + ["spaceship"], + False, + ) + ) + == 16 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-1.com", + "rick@example.com", + True, + False, + ) + ) + == 16 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-1.com", + "jerry@example.com", + True, + True, + ) + ) + == 16 + ) - # jerry's archives are public - assert len(crud.search_archives_by_url(db_session, "https://example-2.com", "jerry@example.com", [], True)) == 33 - assert len(crud.search_archives_by_url(db_session, "https://example-2.com", "rick@example.com", [], True)) == 33 + # Jerry's archives are public + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-2.com", + "jerry@example.com", + [], + True, + ) + ) + == 33 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-2.com", + "rick@example.com", + [], + True, + ) + ) + == 33 + ) # fuzzy search - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False)) == 100 - assert len(crud.search_archives_by_url(db_session, "https://EXAMPLE", ALLOW_ANY_EMAIL, False, False)) == 100 - assert len(crud.search_archives_by_url(db_session, "2.com", ALLOW_ANY_EMAIL, False, False)) == 33 + assert ( + len( + crud.search_archives_by_url( + db_session, "https://example", ALLOW_ANY_EMAIL, False, False + ) + ) + == 100 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, "https://EXAMPLE", ALLOW_ANY_EMAIL, False, False + ) + ) + == 100 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, "2.com", ALLOW_ANY_EMAIL, False, False + ) + ) + == 33 + ) # absolute search - assert len(crud.search_archives_by_url(db_session, "example-2.com", ALLOW_ANY_EMAIL, [], False, absolute_search=True)) == 0 - assert len(crud.search_archives_by_url(db_session, "https://example-2.com", ALLOW_ANY_EMAIL, [], False, absolute_search=True)) == 33 + assert ( + len( + crud.search_archives_by_url( + db_session, + "example-2.com", + ALLOW_ANY_EMAIL, + [], + False, + absolute_search=True, + ) + ) + == 0 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example-2.com", + ALLOW_ANY_EMAIL, + [], + False, + absolute_search=True, + ) + ) + == 33 + ) # archived_after - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, True, True, archived_after=datetime(2010, 1, 1))) == 100 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_after=datetime(2021, 1, 15))) == 70 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_after=datetime(2031, 1, 1))) == 0 + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + True, + True, + archived_after=datetime(2010, 1, 1), + ) + ) + == 100 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + False, + False, + archived_after=datetime(2021, 1, 15), + ) + ) + == 70 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + False, + False, + archived_after=datetime(2031, 1, 1), + ) + ) + == 0 + ) # archived before - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_before=datetime(2010, 1, 1))) == 0 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_before=datetime(2021, 1, 15))) == 28 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_before=datetime(2031, 1, 1))) == 100 + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + False, + False, + archived_before=datetime(2010, 1, 1), + ) + ) + == 0 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + False, + False, + archived_before=datetime(2021, 1, 15), + ) + ) + == 28 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + False, + False, + archived_before=datetime(2031, 1, 1), + ) + ) + == 100 + ) # archived before and after - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_after=datetime(2001, 1, 1), archived_before=datetime(2031, 1, 11))) == 100 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, archived_after=datetime(2021, 1, 14), archived_before=datetime(2021, 1, 16))) == 2 + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + False, + False, + archived_after=datetime(2001, 1, 1), + archived_before=datetime(2031, 1, 11), + ) + ) + == 100 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + False, + False, + archived_after=datetime(2021, 1, 14), + archived_before=datetime(2021, 1, 16), + ) + ) + == 2 + ) # limit - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, limit=10)) == 10 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, limit=-1)) == 1 + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + False, + False, + limit=10, + ) + ) + == 10 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + False, + False, + limit=-1, + ) + ) + == 1 + ) # skip - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, False, False, skip=10)) == 90 + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + False, + False, + skip=10, + ) + ) + == 90 + ) def test_search_archives_by_email(test_data, db_session): - from app.web.config import ALLOW_ANY_EMAIL - # lower/upper case - assert len(crud.search_archives_by_email(db_session, "rick@example.com")) == 34 + assert ( + len(crud.search_archives_by_email(db_session, "rick@example.com")) == 34 + ) # ALLOW_ANY_EMAIL is not a user assert len(crud.search_archives_by_email(db_session, ALLOW_ANY_EMAIL)) == 0 @@ -138,45 +392,108 @@ def test_search_archives_by_email(test_data, db_session): @patch("app.web.db.crud.DATABASE_QUERY_LIMIT", new=25) def test_max_query_limit(test_data, db_session): - from app.web.config import ALLOW_ANY_EMAIL + assert ( + len( + crud.search_archives_by_url( + db_session, "https://example", ALLOW_ANY_EMAIL, [], False + ) + ) + == 25 + ) + assert ( + len( + crud.search_archives_by_url( + db_session, + "https://example", + ALLOW_ANY_EMAIL, + True, + True, + limit=1000, + ) + ) + == 25 + ) - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, [], False)) == 25 - assert len(crud.search_archives_by_url(db_session, "https://example", ALLOW_ANY_EMAIL, True, True, limit=1000)) == 25 - - assert len(crud.search_archives_by_email(db_session, "rick@example.com")) == 25 - assert len(crud.search_archives_by_email(db_session, "rick@example.com", limit=1000)) == 25 + assert ( + len(crud.search_archives_by_email(db_session, "rick@example.com")) == 25 + ) + assert ( + len( + crud.search_archives_by_email( + db_session, "rick@example.com", limit=1000 + ) + ) + == 25 + ) def test_soft_delete(test_data, db_session): # none deleted yet - db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").first() is not None - assert db_session.query(models.Archive).filter(models.Archive.deleted == True).count() == 0 + assert ( + db_session.query(models.Archive) + .filter(models.Archive.id == "archive-id-456-0") + .first() + is not None + ) + assert ( + db_session.query(models.Archive) + .filter(models.Archive.deleted.is_(true())) + .count() + == 0 + ) # delete - assert crud.soft_delete_archive(db_session, "archive-id-456-0", "rick@example.com") == True + assert ( + crud.soft_delete_archive( + db_session, "archive-id-456-0", "rick@example.com" + ) + is True + ) # ensure soft delete - assert db_session.query(models.Archive).filter(models.Archive.deleted == True).count() == 1 - db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").first() is None + assert ( + db_session.query(models.Archive) + .filter(models.Archive.deleted.is_(true())) + .count() + == 1 + ) + assert ( + db_session.query(models.Archive) + .filter(models.Archive.id == "archive-id-456-0") + .filter(models.Archive.deleted.is_(false())) + .first() + is None + ) # already deleted - assert crud.soft_delete_archive(db_session, "archive-id-456-0", "rick@example.com") == False + assert ( + crud.soft_delete_archive( + db_session, "archive-id-456-0", "rick@example.com" + ) + is False + ) def test_count_archives(test_data, db_session): assert crud.count_archives(db_session) == 100 - db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").delete() + db_session.query(models.Archive).filter( + models.Archive.id == "archive-id-456-0" + ).delete() db_session.commit() assert crud.count_archives(db_session) == 99 def test_count_archive_urls(test_data, db_session): assert crud.count_archive_urls(db_session) == 1000 - db_session.query(models.ArchiveUrl).filter(models.ArchiveUrl.url == "https://example-0.com/0").delete() + db_session.query(models.ArchiveUrl).filter( + models.ArchiveUrl.url == "https://example-0.com/0" + ).delete() db_session.commit() assert crud.count_archive_urls(db_session) == 999 - db_session.query(models.Archive).filter(models.Archive.id == "archive-id-456-0").delete() + db_session.query(models.Archive).filter( + models.Archive.id == "archive-id-456-0" + ).delete() db_session.commit() # no Cascade is enabled assert crud.count_archives(db_session) == 99 @@ -185,16 +502,23 @@ def test_count_archive_urls(test_data, db_session): def test_count_users(test_data, db_session): assert crud.count_users(db_session) == 3 - db_session.query(models.User).filter(models.User.email == "rick@example.com").delete() + db_session.query(models.User).filter( + models.User.email == "rick@example.com" + ).delete() db_session.commit() assert crud.count_users(db_session) == 2 def test_count_by_users_since(test_data, db_session): - from app.web.db import crud - # 100y window - assert len(cu := crud.count_by_user_since(db_session, 60 * 60 * 24 * 31 * 12 * 100)) == 3 + assert ( + len( + cu := crud.count_by_user_since( + db_session, 60 * 60 * 24 * 31 * 12 * 100 + ) + ) + == 3 + ) assert cu[0].total == 34 assert cu[1].total == 33 assert cu[2].total == 33 @@ -203,9 +527,18 @@ def test_count_by_users_since(test_data, db_session): def test_upsert_group(test_data, db_session): assert db_session.query(models.Group).count() == 4 - repeatable_params = ["desc 1", "orch.yaml", "sheet.yaml", "service_account_email@example.com", {"read": ["all"]}, ["example.com"]] + repeatable_params = [ + "desc 1", + "orch.yaml", + "sheet.yaml", + "service_account_email@example.com", + {"read": ["all"]}, + ["example.com"], + ] - assert (g1 := crud.upsert_group(db_session, "spaceship", *repeatable_params)) is not None + assert ( + g1 := crud.upsert_group(db_session, "spaceship", *repeatable_params) + ) is not None assert g1.id == "spaceship" assert g1.description == "desc 1" assert g1.orchestrator == "orch.yaml" @@ -214,14 +547,25 @@ def test_upsert_group(test_data, db_session): assert g1.permissions == {"read": ["all"]} assert g1.domains == ["example.com"] assert len(g1.users) == 2 - assert [u.email for u in g1.users] == ["rick@example.com", "morty@example.com"] + assert [u.email for u in g1.users] == [ + "rick@example.com", + "morty@example.com", + ] - assert (g2 := crud.upsert_group(db_session, "interdimensional", *repeatable_params)) is not None + assert ( + g2 := crud.upsert_group( + db_session, "interdimensional", *repeatable_params + ) + ) is not None assert g2.id == "interdimensional" assert len(g2.users) == 1 assert [u.email for u in g2.users] == ["rick@example.com"] - assert (g3 := crud.upsert_group(db_session, "this-is-a-new-group", *repeatable_params)) is not None + assert ( + g3 := crud.upsert_group( + db_session, "this-is-a-new-group", *repeatable_params + ) + ) is not None assert g3.id == "this-is-a-new-group" assert len(g3.users) == 0 @@ -229,29 +573,38 @@ def test_upsert_group(test_data, db_session): def test_upsert_user_groups(db_session): - @patch('app.web.db.crud.get_settings', new=lambda: bad_setings) + @patch("app.web.db.crud.get_settings", new=lambda: bad_settings) def test_missing_yaml(db_session): with pytest.raises(FileNotFoundError): crud.upsert_user_groups(db_session) - @patch('app.web.db.crud.get_settings', new=lambda: bad_setings) + @patch("app.web.db.crud.get_settings", new=lambda: bad_settings) def test_broken_yaml(db_session): with pytest.raises(yaml.YAMLError): crud.upsert_user_groups(db_session) - bad_setings = Settings(_env_file=".env.test") + bad_settings = Settings(_env_file=".env.test") - bad_setings.USER_GROUPS_FILENAME = "app/tests/user-groups.test.missing.yaml" + bad_settings.USER_GROUPS_FILENAME = ( + "app/tests/user-groups.test.missing.yaml" + ) test_missing_yaml(db_session) - bad_setings.USER_GROUPS_FILENAME = "app/tests/user-groups.test.broken.yaml" + bad_settings.USER_GROUPS_FILENAME = "app/tests/user-groups.test.broken.yaml" test_broken_yaml(db_session) def test_create_sheet(db_session): assert db_session.query(models.Sheet).count() == 0 - s = crud.create_sheet(db_session, "sheet-id-123", "sheet name", "email@example.com", "group-id", "hourly") + s = crud.create_sheet( + db_session, + "sheet-id-123", + "sheet name", + "email@example.com", + "group-id", + "hourly", + ) assert s is not None assert s.id == "sheet-id-123" assert s.name == "sheet name" @@ -261,19 +614,35 @@ def test_create_sheet(db_session): assert db_session.query(models.Sheet).count() == 1 - # duplicate id - import sqlalchemy with pytest.raises(sqlalchemy.exc.IntegrityError): - crud.create_sheet(db_session, "sheet-id-123", "I thought this was another sheet", "email", "group-id", "hourly") + crud.create_sheet( + db_session, + "sheet-id-123", + "I thought this was another sheet", + "email", + "group-id", + "hourly", + ) def test_get_user_sheet(test_data, db_session): assert crud.get_user_sheet(db_session, "", "sheet-0") is None - assert crud.get_user_sheet(db_session, "morty@example.com", "sheet-0") is None + assert ( + crud.get_user_sheet(db_session, "morty@example.com", "sheet-0") is None + ) - assert crud.get_user_sheet(db_session, "rick@example.com", "sheet-0") is not None - assert crud.get_user_sheet(db_session, "rick@example.com", "sheet-0-2") is not None - assert crud.get_user_sheet(db_session, "morty@example.com", "sheet-1") is not None + assert ( + crud.get_user_sheet(db_session, "rick@example.com", "sheet-0") + is not None + ) + assert ( + crud.get_user_sheet(db_session, "rick@example.com", "sheet-0-2") + is not None + ) + assert ( + crud.get_user_sheet(db_session, "morty@example.com", "sheet-1") + is not None + ) def test_get_user_sheets(test_data, db_session): @@ -285,9 +654,9 @@ def test_get_user_sheets(test_data, db_session): def test_delete_sheet(test_data, db_session): - assert crud.delete_sheet(db_session, "sheet-0", "") == False - assert crud.delete_sheet(db_session, "sheet-0", "rick@example.com") == True - assert crud.delete_sheet(db_session, "sheet-0", "rick@example.com") == False + assert crud.delete_sheet(db_session, "sheet-0", "") is False + assert crud.delete_sheet(db_session, "sheet-0", "rick@example.com") is True + assert crud.delete_sheet(db_session, "sheet-0", "rick@example.com") is False @pytest.mark.asyncio @@ -299,21 +668,21 @@ async def test_find_by_store_until(async_db_session): url="https://example-expired-1.com", result={}, author_id="rick@example.com", - store_until=now - timedelta(days=1) + store_until=now - timedelta(days=1), ) archive2 = models.Archive( id="archive-expired-2", url="https://example-expired-2.com", result={}, author_id="rick@example.com", - store_until=now - timedelta(hours=1) + store_until=now - timedelta(hours=1), ) archive3 = models.Archive( id="archive-active", url="https://example-active.com", result={}, author_id="rick@example.com", - store_until=now + timedelta(days=1) + store_until=now + timedelta(days=1), ) async_db_session.add_all([archive1, archive2, archive3]) await async_db_session.commit() @@ -323,11 +692,15 @@ async def test_find_by_store_until(async_db_session): assert len(list(expired)) == 2 # Should find 1 archive expired before 2 hours ago - expired = await crud.find_by_store_until(async_db_session, now - timedelta(hours=2)) + expired = await crud.find_by_store_until( + async_db_session, now - timedelta(hours=2) + ) assert len(list(expired)) == 1 # Should find no archives expired before 2 days ago - expired = await crud.find_by_store_until(async_db_session, now - timedelta(days=2)) + expired = await crud.find_by_store_until( + async_db_session, now - timedelta(days=2) + ) assert len(list(expired)) == 0 # Should not find deleted archives @@ -339,45 +712,78 @@ async def test_find_by_store_until(async_db_session): @pytest.mark.asyncio async def test_get_sheets_by_id_hash(async_db_session): + author_emails = ["rick@example.com", "morty@example.com", "jerry@example.com"] + # Add test data - authors = ["rick@example.com", "morty@example.com", "jerry@example.com"] sheets = [ - models.Sheet(id="sheet-0", name="sheet-0", author_id=authors[0], group_id=None, frequency="daily"), - models.Sheet(id="sheet-0-2", name="sheet-0-2", author_id=authors[0], group_id="spaceship", frequency="hourly"), - models.Sheet(id="sheet-1", name="sheet-1", author_id=authors[1], group_id=None, frequency="daily"), - models.Sheet(id="sheet-2", name="sheet-2", author_id=authors[2], group_id=None, frequency="daily") + models.Sheet( + id="sheet-0", + name="sheet-0", + author_id=author_emails[0], + group_id=None, + frequency="daily", + ), + models.Sheet( + id="sheet-0-2", + name="sheet-0-2", + author_id=author_emails[0], + group_id="spaceship", + frequency="hourly", + ), + models.Sheet( + id="sheet-1", + name="sheet-1", + author_id=author_emails[1], + group_id=None, + frequency="daily", + ), + models.Sheet( + id="sheet-2", + name="sheet-2", + author_id=author_emails[2], + group_id=None, + frequency="daily", + ), ] async_db_session.add_all(sheets) await async_db_session.commit() with patch("app.web.db.crud.fnv1a_hash_mod", return_value=1): # Test retrieving hourly sheets - hourly_sheets = await crud.get_sheets_by_id_hash(async_db_session, "hourly", 4, 1) + hourly_sheets = await crud.get_sheets_by_id_hash( + async_db_session, "hourly", 4, 1 + ) assert len(hourly_sheets) == 1 assert hourly_sheets[0].id == "sheet-0-2" assert hourly_sheets[0].frequency == "hourly" # Test retrieving daily sheets - daily_sheets = await crud.get_sheets_by_id_hash(async_db_session, "daily", 4, 1) + daily_sheets = await crud.get_sheets_by_id_hash( + async_db_session, "daily", 4, 1 + ) assert len(daily_sheets) == 3 assert all(sheet.frequency == "daily" for sheet in daily_sheets) - assert {sheet.id for sheet in daily_sheets} == {"sheet-0", "sheet-1", "sheet-2"} + assert {sheet.id for sheet in daily_sheets} == { + "sheet-0", + "sheet-1", + "sheet-2", + } # Test with non-matching hash - no_sheets = await crud.get_sheets_by_id_hash(async_db_session, "daily", 4, 3) + no_sheets = await crud.get_sheets_by_id_hash( + async_db_session, "daily", 4, 3 + ) assert len(no_sheets) == 0 # Test with non-existent frequency - weekly_sheets = await crud.get_sheets_by_id_hash(async_db_session, "weekly", 4, 1) + weekly_sheets = await crud.get_sheets_by_id_hash( + async_db_session, "weekly", 4, 1 + ) assert len(weekly_sheets) == 0 @pytest.mark.asyncio async def test_delete_stale_sheets(async_db_session): - from datetime import datetime, timedelta - - from sqlalchemy.sql import select - now = datetime.now() active_date = now - timedelta(days=5) stale_date = now - timedelta(days=15) @@ -389,29 +795,29 @@ async def test_delete_stale_sheets(async_db_session): name="Active Sheet 1", author_id="rick@example.com", frequency="daily", - last_url_archived_at=active_date + last_url_archived_at=active_date, ), models.Sheet( id="sheet-active-2", name="Active Sheet 2", author_id="morty@example.com", frequency="hourly", - last_url_archived_at=active_date + last_url_archived_at=active_date, ), models.Sheet( id="sheet-stale-1", name="Stale Sheet 1", author_id="rick@example.com", frequency="daily", - last_url_archived_at=stale_date + last_url_archived_at=stale_date, ), models.Sheet( id="sheet-stale-2", name="Stale Sheet 2", author_id="morty@example.com", frequency="daily", - last_url_archived_at=stale_date - ) + last_url_archived_at=stale_date, + ), ] async_db_session.add_all(sheets) await async_db_session.commit() diff --git a/app/tests/web/db/test_user_state.py b/app/tests/web/db/test_user_state.py index 665bf08..5d18cea 100644 --- a/app/tests/web/db/test_user_state.py +++ b/app/tests/web/db/test_user_state.py @@ -1,4 +1,3 @@ - from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -6,6 +5,7 @@ import pytest from app.shared.db import models from app.shared.user_groups import GroupInfo, GroupPermissions from app.web.db.user_state import UserState +from app.web.utils.misc import convert_priority_to_queue_dict def fresh_user_state(): @@ -21,39 +21,73 @@ def user_state(): def user_state_with_groups(user_state): user_groups = [ models.Group(id="no-permissions", permissions={}), - models.Group(id="group1", description="this is g1", service_account_email="sa1@example.com", permissions={"read": ["group1", "no-permissions"], "read_public": True, "archive_url": True, "archive_sheet": True, "max_archive_lifespan_months": 24, "max_monthly_urls": 100, "max_monthly_mbs": 1000, "priority": "high"}), - models.Group(id="group2", description="this is g2", service_account_email="sa2@example.com", permissions={"read": ["all"], "read_public": True, "archive_url": False, "archive_sheet": False, "max_archive_lifespan_months": -1, "max_monthly_urls": -1, "max_monthly_mbs": -1, "priority": "low", "sheet_frequency": {"daily"}}), + models.Group( + id="group1", + description="this is g1", + service_account_email="sa1@example.com", + permissions={ + "read": ["group1", "no-permissions"], + "read_public": True, + "archive_url": True, + "archive_sheet": True, + "max_archive_lifespan_months": 24, + "max_monthly_urls": 100, + "max_monthly_mbs": 1000, + "priority": "high", + }, + ), + models.Group( + id="group2", + description="this is g2", + service_account_email="sa2@example.com", + permissions={ + "read": ["all"], + "read_public": True, + "archive_url": False, + "archive_sheet": False, + "max_archive_lifespan_months": -1, + "max_monthly_urls": -1, + "max_monthly_mbs": -1, + "priority": "low", + "sheet_frequency": {"daily"}, + }, + ), ] - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=user_groups): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=user_groups, + ): yield user_state def test_permissions(user_state_with_groups): permissions = user_state_with_groups.permissions - assert permissions["all"].read == True - assert permissions["all"].read_public == True - assert permissions["all"].archive_url == True - assert permissions["all"].archive_sheet == True + assert permissions["all"].read is True + assert permissions["all"].read_public is True + assert permissions["all"].archive_url is True + assert permissions["all"].archive_sheet is True assert permissions["all"].max_archive_lifespan_months == -1 assert permissions["all"].max_monthly_urls == -1 assert permissions["all"].max_monthly_mbs == -1 assert permissions["all"].priority == "high" - assert permissions["group1"].read == set(["group1", "no-permissions"]) - assert permissions["group1"].read_public == True - assert permissions["group1"].archive_url == True - assert permissions["group1"].archive_sheet == True + assert permissions["group1"].read == {"group1", "no-permissions"} + assert permissions["group1"].read_public is True + assert permissions["group1"].archive_url is True + assert permissions["group1"].archive_sheet is True assert permissions["group1"].max_archive_lifespan_months == 24 assert permissions["group1"].max_monthly_urls == 100 assert permissions["group1"].max_monthly_mbs == 1000 assert permissions["group1"].priority == "high" - assert permissions["group2"].read == set(["all"]) - assert permissions["group2"].read_public == True - assert permissions["group2"].archive_url == False - assert permissions["group2"].archive_sheet == False + assert permissions["group2"].read == {"all"} + assert permissions["group2"].read_public is True + assert permissions["group2"].archive_url is False + assert permissions["group2"].archive_sheet is False assert permissions["group2"].max_archive_lifespan_months == -1 assert permissions["group2"].max_monthly_urls == -1 assert permissions["group2"].max_monthly_mbs == -1 @@ -63,13 +97,19 @@ def test_permissions(user_state_with_groups): def test_user_groups_names(user_state): - with patch('app.web.db.crud.get_user_group_names', return_value=["group1", "group2"]) as mock: + with patch( + "app.web.db.crud.get_user_group_names", + return_value=["group1", "group2"], + ) as mock: assert user_state.user_groups_names == ["group1", "group2", "default"] mock.assert_called_once_with(None, "test@example.com") def test_user_groups(user_state): - with patch('app.web.db.crud.get_user_groups_by_name', return_value=[MagicMock(), MagicMock()]) as mock: + with patch( + "app.web.db.crud.get_user_groups_by_name", + return_value=[MagicMock(), MagicMock()], + ) as mock: user_state._user_groups_names = ["group1", "group2"] assert len(user_state.user_groups) == 2 mock.assert_called_once_with(None, ["group1", "group2"]) @@ -78,85 +118,166 @@ def test_user_groups(user_state): def test_read(): us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[models.Group(id="no-permissions", permissions={})], + ) as mock: assert not hasattr(us, "_read") assert us.read == set() assert us._read == set() mock.assert_called_once() us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"read": ["group1", "no-permissions"]})]): - assert us.read == set(["group1", "no-permissions"]) + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group( + id="group1", permissions={"read": ["group1", "no-permissions"]} + ) + ], + ): + assert us.read == {"group1", "no-permissions"} us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"read": ["all"]})]): - assert us.read == True + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[models.Group(id="group1", permissions={"read": ["all"]})], + ): + assert us.read is True def test_read_public(): us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[models.Group(id="no-permissions", permissions={})], + ) as mock: assert not hasattr(us, "_read_public") - assert us.read_public == False - assert us._read_public == False + assert us.read_public is False + assert us._read_public is False mock.assert_called_once() # no new calls - assert us.read_public == False + assert us.read_public is False mock.assert_called_once() us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"read_public": True})]): - assert us.read_public == True + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"read_public": True}) + ], + ): + assert us.read_public is True us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"read_public": False})]): - assert us.read_public == False + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"read_public": False}) + ], + ): + assert us.read_public is False def test_archive_url(): us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[models.Group(id="no-permissions", permissions={})], + ) as mock: assert not hasattr(us, "_archive_url") - assert us.archive_url == False - assert us._archive_url == False + assert us.archive_url is False + assert us._archive_url is False mock.assert_called_once() # no new calls - assert us.archive_url == False + assert us.archive_url is False mock.assert_called_once() us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"archive_url": False})]): - assert us.archive_url == False + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"archive_url": False}) + ], + ): + assert us.archive_url is False us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"archive_url": True})]): - assert us.archive_url == True + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"archive_url": True}) + ], + ): + assert us.archive_url is True def test_archive_sheet(): us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[models.Group(id="no-permissions", permissions={})], + ) as mock: assert not hasattr(us, "_archive_sheet") - assert us.archive_sheet == False - assert us._archive_sheet == False + assert us.archive_sheet is False + assert us._archive_sheet is False mock.assert_called_once() # no new calls - assert us.archive_sheet == False + assert us.archive_sheet is False mock.assert_called_once() us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"archive_sheet": False})]): - assert us.archive_sheet == False + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"archive_sheet": False}) + ], + ): + assert us.archive_sheet is False us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"archive_sheet": True})]): - assert us.archive_sheet == True + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"archive_sheet": True}) + ], + ): + assert us.archive_sheet is True def test_sheet_frequency(): us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[models.Group(id="no-permissions", permissions={})], + ) as mock: assert not hasattr(us, "_sheet_frequency") assert us.sheet_frequency == set() assert us._sheet_frequency == set() @@ -166,18 +287,42 @@ def test_sheet_frequency(): mock.assert_called_once() us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"sheet_frequency": ["daily", "hourly"]})]): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group( + id="group1", + permissions={"sheet_frequency": ["daily", "hourly"]}, + ) + ], + ): assert us.sheet_frequency == {"daily", "hourly"} us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"sheet_frequency": []})]): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"sheet_frequency": []}) + ], + ): assert us.sheet_frequency == set() def test_max_archive_lifespan_months(): us = fresh_user_state() - default = GroupPermissions.model_fields["max_archive_lifespan_months"].default - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + default = GroupPermissions.model_fields[ + "max_archive_lifespan_months" + ].default + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[models.Group(id="no-permissions", permissions={})], + ) as mock: assert not hasattr(us, "_max_archive_lifespan_months") assert us.max_archive_lifespan_months == default assert us._max_archive_lifespan_months == default @@ -187,18 +332,44 @@ def test_max_archive_lifespan_months(): mock.assert_called_once() us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_archive_lifespan_months": 24})]): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group( + id="group1", permissions={"max_archive_lifespan_months": 24} + ) + ], + ): assert us.max_archive_lifespan_months == 24 us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_archive_lifespan_months": 150}), models.Group(id="group2", permissions={"max_archive_lifespan_months": -1})]): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group( + id="group1", permissions={"max_archive_lifespan_months": 150} + ), + models.Group( + id="group2", permissions={"max_archive_lifespan_months": -1} + ), + ], + ): assert us.max_archive_lifespan_months == -1 def test_max_monthly_urls(): us = fresh_user_state() default = GroupPermissions.model_fields["max_monthly_urls"].default - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[models.Group(id="no-permissions", permissions={})], + ) as mock: assert not hasattr(us, "_max_monthly_urls") assert us.max_monthly_urls == default assert us._max_monthly_urls == default @@ -208,18 +379,38 @@ def test_max_monthly_urls(): mock.assert_called_once() us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_monthly_urls": 100})]): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"max_monthly_urls": 100}) + ], + ): assert us.max_monthly_urls == 100 us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_monthly_urls": 150}), models.Group(id="group2", permissions={"max_monthly_urls": -1})]): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"max_monthly_urls": 150}), + models.Group(id="group2", permissions={"max_monthly_urls": -1}), + ], + ): assert us.max_monthly_urls == -1 def test_max_monthly_mbs(): us = fresh_user_state() default = GroupPermissions.model_fields["max_monthly_mbs"].default - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[models.Group(id="no-permissions", permissions={})], + ) as mock: assert not hasattr(us, "_max_monthly_mbs") assert us.max_monthly_mbs == default assert us._max_monthly_mbs == default @@ -229,17 +420,37 @@ def test_max_monthly_mbs(): mock.assert_called_once() us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_monthly_mbs": 1000})]): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"max_monthly_mbs": 1000}) + ], + ): assert us.max_monthly_mbs == 1000 us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"max_monthly_mbs": 1500}), models.Group(id="group2", permissions={"max_monthly_mbs": -1})]): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"max_monthly_mbs": 1500}), + models.Group(id="group2", permissions={"max_monthly_mbs": -1}), + ], + ): assert us.max_monthly_mbs == -1 def test_priority(user_state): default = GroupPermissions.model_fields["priority"].default - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="no-permissions", permissions={})]) as mock: + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[models.Group(id="no-permissions", permissions={})], + ) as mock: assert not hasattr(user_state, "_priority") assert user_state.priority == default assert user_state._priority == default @@ -249,11 +460,26 @@ def test_priority(user_state): mock.assert_called_once() us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"priority": "high"})]): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"priority": "high"}) + ], + ): assert us.priority == "high" us = fresh_user_state() - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[models.Group(id="group1", permissions={"priority": "low"}), models.Group(id="group2", permissions={"priority": "medium"})]): + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"priority": "low"}), + models.Group(id="group2", permissions={"priority": "medium"}), + ], + ): assert us.priority == "low" @@ -263,21 +489,45 @@ def test_active(): (True, False, False, False, True), (False, True, False, False, True), (False, False, True, False, True), - (False, False, False, True, True) + (False, False, False, True, True), ]: us = fresh_user_state() - with patch.object(UserState, 'read', new_callable=PropertyMock, return_value=read), \ - patch.object(UserState, 'read_public', new_callable=PropertyMock, return_value=read_public), \ - patch.object(UserState, 'archive_url', new_callable=PropertyMock, return_value=archive_url), \ - patch.object(UserState, 'archive_sheet', new_callable=PropertyMock, return_value=archive_sheet): + with ( + patch.object( + UserState, "read", new_callable=PropertyMock, return_value=read + ), + patch.object( + UserState, + "read_public", + new_callable=PropertyMock, + return_value=read_public, + ), + patch.object( + UserState, + "archive_url", + new_callable=PropertyMock, + return_value=archive_url, + ), + patch.object( + UserState, + "archive_sheet", + new_callable=PropertyMock, + return_value=archive_sheet, + ), + ): assert us.active == is_active def test_in_group(user_state): - with patch.object(UserState, 'user_groups_names', new_callable=PropertyMock, return_value=["group1", "group2"]): - assert user_state.in_group("group1") == True - assert user_state.in_group("group2") == True - assert user_state.in_group("group3") == False + with patch.object( + UserState, + "user_groups_names", + new_callable=PropertyMock, + return_value=["group1", "group2"], + ): + assert user_state.in_group("group1") is True + assert user_state.in_group("group2") is True + assert user_state.in_group("group3") is False def test_usage(db_session): @@ -295,10 +545,34 @@ def test_usage(db_session): ] megabytes = int(sum(bytes) / 1024 / 1024) - with patch.object(db_session, 'query', side_effect=[ - MagicMock(filter=MagicMock(return_value=MagicMock(group_by=MagicMock(return_value=MagicMock(all=MagicMock(return_value=user_sheets)))))), - MagicMock(filter=MagicMock(return_value=MagicMock(group_by=MagicMock(return_value=MagicMock(all=MagicMock(return_value=urls_by_group)))))) - ]): + with patch.object( + db_session, + "query", + side_effect=[ + MagicMock( + filter=MagicMock( + return_value=MagicMock( + group_by=MagicMock( + return_value=MagicMock( + all=MagicMock(return_value=user_sheets) + ) + ) + ) + ) + ), + MagicMock( + filter=MagicMock( + return_value=MagicMock( + group_by=MagicMock( + return_value=MagicMock( + all=MagicMock(return_value=urls_by_group) + ) + ) + ) + ) + ), + ], + ): usage_response = user_state.usage() assert usage_response.monthly_urls == 155 @@ -306,11 +580,15 @@ def test_usage(db_session): assert usage_response.total_sheets == 115 assert usage_response.groups["group1"].monthly_urls == 50 - assert usage_response.groups["group1"].monthly_mbs == int(bytes[0] / 1024 / 1024) + assert usage_response.groups["group1"].monthly_mbs == int( + bytes[0] / 1024 / 1024 + ) assert usage_response.groups["group1"].total_sheets == 5 assert usage_response.groups["group2"].monthly_urls == 100 - assert usage_response.groups["group2"].monthly_mbs == int(bytes[1] / 1024 / 1024) + assert usage_response.groups["group2"].monthly_mbs == int( + bytes[1] / 1024 / 1024 + ) assert usage_response.groups["group2"].total_sheets == 10 assert usage_response.groups["group3"].monthly_urls == 0 @@ -318,7 +596,9 @@ def test_usage(db_session): assert usage_response.groups["group3"].total_sheets == 100 assert usage_response.groups["group4"].monthly_urls == 5 - assert usage_response.groups["group4"].monthly_mbs == int(bytes[2] / 1024 / 1024) + assert usage_response.groups["group4"].monthly_mbs == int( + bytes[2] / 1024 / 1024 + ) assert usage_response.groups["group4"].total_sheets == 0 @@ -334,8 +614,23 @@ def test_has_quota_monthly_sheets(db_session): ] for permissions, count, expected in test_cases: - with patch.object(UserState, 'permissions', new_callable=PropertyMock, return_value=permissions): - with patch.object(us.db, 'query', return_value=MagicMock(filter=MagicMock(return_value=MagicMock(count=MagicMock(return_value=count))))): + with patch.object( + UserState, + "permissions", + new_callable=PropertyMock, + return_value=permissions, + ): + with patch.object( + us.db, + "query", + return_value=MagicMock( + filter=MagicMock( + return_value=MagicMock( + count=MagicMock(return_value=count) + ) + ) + ), + ): assert us.has_quota_monthly_sheets("group1") == expected @@ -350,8 +645,23 @@ def test_has_quota_max_monthly_urls(db_session): ] for permissions, count, expected in test_cases: - with patch.object(UserState, 'permissions', new_callable=PropertyMock, return_value=permissions): - with patch.object(us.db, 'query', return_value=MagicMock(filter=MagicMock(return_value=MagicMock(count=MagicMock(return_value=count))))): + with patch.object( + UserState, + "permissions", + new_callable=PropertyMock, + return_value=permissions, + ): + with patch.object( + us.db, + "query", + return_value=MagicMock( + filter=MagicMock( + return_value=MagicMock( + count=MagicMock(return_value=count) + ) + ) + ), + ): assert us.has_quota_max_monthly_urls("group1") == expected test_cases = [ (-1, 1000, True), @@ -361,8 +671,23 @@ def test_has_quota_max_monthly_urls(db_session): ] for max_urls, count, expected in test_cases: - with patch.object(UserState, 'max_monthly_urls', new_callable=PropertyMock, return_value=max_urls): - with patch.object(us.db, 'query', return_value=MagicMock(filter=MagicMock(return_value=MagicMock(count=MagicMock(return_value=count))))): + with patch.object( + UserState, + "max_monthly_urls", + new_callable=PropertyMock, + return_value=max_urls, + ): + with patch.object( + us.db, + "query", + return_value=MagicMock( + filter=MagicMock( + return_value=MagicMock( + count=MagicMock(return_value=count) + ) + ) + ), + ): assert us.has_quota_max_monthly_urls("") == expected @@ -377,8 +702,29 @@ def test_has_quota_max_monthly_mbs(db_session): ] for permissions, mbs, expected in test_cases: - with patch.object(UserState, 'permissions', new_callable=PropertyMock, return_value=permissions): - with patch.object(us.db, 'query', return_value=MagicMock(filter=MagicMock(return_value=MagicMock(with_entities=MagicMock(return_value=MagicMock(scalar=MagicMock(return_value=mbs * 1024 * 1024))))))): + with patch.object( + UserState, + "permissions", + new_callable=PropertyMock, + return_value=permissions, + ): + with patch.object( + us.db, + "query", + return_value=MagicMock( + filter=MagicMock( + return_value=MagicMock( + with_entities=MagicMock( + return_value=MagicMock( + scalar=MagicMock( + return_value=mbs * 1024 * 1024 + ) + ) + ) + ) + ) + ), + ): assert us.has_quota_max_monthly_mbs("group1") == expected test_cases = [ @@ -389,8 +735,29 @@ def test_has_quota_max_monthly_mbs(db_session): ] for max_mbs, mbs, expected in test_cases: - with patch.object(UserState, 'max_monthly_mbs', new_callable=PropertyMock, return_value=max_mbs): - with patch.object(us.db, 'query', return_value=MagicMock(filter=MagicMock(return_value=MagicMock(with_entities=MagicMock(return_value=MagicMock(scalar=MagicMock(return_value=mbs * 1024 * 1024))))))): + with patch.object( + UserState, + "max_monthly_mbs", + new_callable=PropertyMock, + return_value=max_mbs, + ): + with patch.object( + us.db, + "query", + return_value=MagicMock( + filter=MagicMock( + return_value=MagicMock( + with_entities=MagicMock( + return_value=MagicMock( + scalar=MagicMock( + return_value=mbs * 1024 * 1024 + ) + ) + ) + ) + ) + ), + ): assert us.has_quota_max_monthly_mbs("") == expected @@ -400,10 +767,15 @@ def test_can_manually_trigger(user_state): "group2": GroupInfo(manually_trigger_sheet=False), } - with patch.object(UserState, 'permissions', new_callable=PropertyMock, return_value=permissions): - assert user_state.can_manually_trigger("group1") == True - assert user_state.can_manually_trigger("group2") == False - assert user_state.can_manually_trigger("group3") == False + with patch.object( + UserState, + "permissions", + new_callable=PropertyMock, + return_value=permissions, + ): + assert user_state.can_manually_trigger("group1") is True + assert user_state.can_manually_trigger("group2") is False + assert user_state.can_manually_trigger("group3") is False def test_is_sheet_frequency_allowed(user_state): @@ -412,23 +784,44 @@ def test_is_sheet_frequency_allowed(user_state): "group2": GroupInfo(sheet_frequency={"daily"}), } - with patch.object(UserState, 'permissions', new_callable=PropertyMock, return_value=permissions): - assert user_state.is_sheet_frequency_allowed("group1", "daily") == True - assert user_state.is_sheet_frequency_allowed("group1", "hourly") == True - assert user_state.is_sheet_frequency_allowed("group1", "weekly") == False - assert user_state.is_sheet_frequency_allowed("group2", "hourly") == False - assert user_state.is_sheet_frequency_allowed("group2", "daily") == True - assert user_state.is_sheet_frequency_allowed("group3", "daily") == False + with patch.object( + UserState, + "permissions", + new_callable=PropertyMock, + return_value=permissions, + ): + assert user_state.is_sheet_frequency_allowed("group1", "daily") is True + assert user_state.is_sheet_frequency_allowed("group1", "hourly") is True + assert ( + user_state.is_sheet_frequency_allowed("group1", "weekly") is False + ) + assert ( + user_state.is_sheet_frequency_allowed("group2", "hourly") is False + ) + assert user_state.is_sheet_frequency_allowed("group2", "daily") is True + assert user_state.is_sheet_frequency_allowed("group3", "daily") is False def test_priority_group(user_state): - from app.web.utils.misc import convert_priority_to_queue_dict - with patch.object(UserState, 'user_groups', new_callable=PropertyMock, return_value=[ - models.Group(id="group1", permissions={"priority": "high"}), - models.Group(id="group2", permissions={"priority": "medium"}), - models.Group(id="group3", permissions={"priority": "low"}), - ]): - assert user_state.priority_group("group1") == convert_priority_to_queue_dict("high") - assert user_state.priority_group("group2") == convert_priority_to_queue_dict("medium") - assert user_state.priority_group("group3") == convert_priority_to_queue_dict("low") - assert user_state.priority_group("group4") == convert_priority_to_queue_dict("low") + with patch.object( + UserState, + "user_groups", + new_callable=PropertyMock, + return_value=[ + models.Group(id="group1", permissions={"priority": "high"}), + models.Group(id="group2", permissions={"priority": "medium"}), + models.Group(id="group3", permissions={"priority": "low"}), + ], + ): + assert user_state.priority_group( + "group1" + ) == convert_priority_to_queue_dict("high") + assert user_state.priority_group( + "group2" + ) == convert_priority_to_queue_dict("medium") + assert user_state.priority_group( + "group3" + ) == convert_priority_to_queue_dict("low") + assert user_state.priority_group( + "group4" + ) == convert_priority_to_queue_dict("low") diff --git a/app/tests/web/endpoints/test_default.py b/app/tests/web/endpoints/test_default.py index e4e34cc..970e705 100644 --- a/app/tests/web/endpoints/test_default.py +++ b/app/tests/web/endpoints/test_default.py @@ -1,17 +1,20 @@ +from http import HTTPStatus from unittest.mock import MagicMock import pytest from fastapi.testclient import TestClient +from loguru import logger from app.shared.schemas import Usage, UsageResponse from app.shared.user_groups import GroupInfo -from app.tests.web.db.test_crud import test_data from app.web.config import VERSION +from app.web.security import get_user_state +from app.web.utils.metrics import measure_regular_metrics def test_endpoint_home(client_with_auth): r = client_with_auth.get("/") - assert r.status_code == 200 + assert r.status_code == HTTPStatus.OK j = r.json() assert "version" in j and j["version"] == VERSION assert "breakingChanges" in j @@ -20,7 +23,7 @@ def test_endpoint_home(client_with_auth): def test_endpoint_health(client_with_auth): r = client_with_auth.get("/health") - assert r.status_code == 200 + assert r.status_code == HTTPStatus.OK assert r.json() == {"status": "ok"} @@ -31,32 +34,31 @@ def test_endpoint_active_no_auth(client, test_no_auth): def test_endpoint_active(app): m_user_state = MagicMock() - from app.web.security import get_user_state app.dependency_overrides[get_user_state] = lambda: m_user_state # inactive user m_user_state.active = False client = TestClient(app) r = client.get("/user/active") - assert r.status_code == 200 + assert r.status_code == HTTPStatus.OK assert r.json() == {"active": False} # active user m_user_state.active = True client = TestClient(app) r = client.get("/user/active") - assert r.status_code == 200 + assert r.status_code == HTTPStatus.OK assert r.json() == {"active": True} def test_no_serve_local_archive_by_default(client_with_auth): r = client_with_auth.get("/app/local_archive_test/temp.txt") - assert r.status_code == 404 + assert r.status_code == HTTPStatus.NOT_FOUND def test_favicon(client_with_auth): r = client_with_auth.get("/favicon.ico") - assert r.status_code == 200 + assert r.status_code == HTTPStatus.OK assert r.headers["content-type"] == "image/vnd.microsoft.icon" @@ -72,8 +74,10 @@ def test_endpoint_test_prometheus_no_user_auth(client_with_auth, test_no_auth): async def test_prometheus_metrics(test_data, client_with_token, get_settings): # before metrics calculation r = client_with_token.get("/metrics") - assert r.status_code == 200 - assert r.headers["content-type"] == "text/plain; version=0.0.4; charset=utf-8" + assert r.status_code == HTTPStatus.OK + assert ( + r.headers["content-type"] == "text/plain; version=0.0.4; charset=utf-8" + ) assert "disk_utilization" in r.text assert "database_metrics" in r.text assert "exceptions" in r.text @@ -81,8 +85,9 @@ async def test_prometheus_metrics(test_data, client_with_token, get_settings): assert 'disk_utilization{type="used"}' not in r.text # after metrics calculation - from app.web.utils.metrics import measure_regular_metrics - await measure_regular_metrics(get_settings.DATABASE_PATH, 60 * 60 * 24 * 31 * 12 * 100) + await measure_regular_metrics( + get_settings.DATABASE_PATH, 60 * 60 * 24 * 31 * 12 * 100 + ) r2 = client_with_token.get("/metrics") assert 'disk_utilization{type="used"}' in r2.text assert 'disk_utilization{type="free"}' in r2.text @@ -90,20 +95,37 @@ async def test_prometheus_metrics(test_data, client_with_token, get_settings): assert 'database_metrics{query="count_archives"} 100.0' in r2.text assert 'database_metrics{query="count_archive_urls"} 1000.0' in r2.text assert 'database_metrics{query="count_users"} 3.0' in r2.text - assert 'database_metrics_counter_total{query="count_by_user",user="rick@example.com"} 34.0' in r2.text - assert 'database_metrics_counter_total{query="count_by_user",user="morty@example.com"} 33.0' in r2.text - assert 'database_metrics_counter_total{query="count_by_user",user="jerry@example.com"} 33.0' in r2.text + assert ( + 'database_metrics_counter_total{query="count_by_user",user="rick@example.com"} 34.0' + in r2.text + ) + assert ( + 'database_metrics_counter_total{query="count_by_user",user="morty@example.com"} 33.0' + in r2.text + ) + assert ( + 'database_metrics_counter_total{query="count_by_user",user="jerry@example.com"} 33.0' + in r2.text + ) # 30s window, should not change the gauges nor the total in the counters - from app.web.utils.metrics import measure_regular_metrics await measure_regular_metrics(get_settings.DATABASE_PATH, 30) r3 = client_with_token.get("/metrics") assert 'database_metrics{query="count_archives"} 100.0' in r3.text assert 'database_metrics{query="count_archive_urls"} 1000.0' in r3.text assert 'database_metrics{query="count_users"} 3.0' in r3.text - assert 'database_metrics_counter_total{query="count_by_user",user="rick@example.com"} 34.0' in r3.text - assert 'database_metrics_counter_total{query="count_by_user",user="morty@example.com"} 33.0' in r3.text - assert 'database_metrics_counter_total{query="count_by_user",user="jerry@example.com"} 33.0' in r3.text + assert ( + 'database_metrics_counter_total{query="count_by_user",user="rick@example.com"} 34.0' + in r3.text + ) + assert ( + 'database_metrics_counter_total{query="count_by_user",user="morty@example.com"} 33.0' + in r3.text + ) + assert ( + 'database_metrics_counter_total{query="count_by_user",user="jerry@example.com"} 33.0' + in r3.text + ) def test_endpoint_get_user_permissions_no_user_auth(client, test_no_auth): @@ -111,14 +133,12 @@ def test_endpoint_get_user_permissions_no_user_auth(client, test_no_auth): def test_endpoint_get_user_permissions(app): - from app.web.security import get_user_state - m_user_state = MagicMock() rv = { "all": GroupInfo(read=True), "group1": GroupInfo(archive_url=True), } - from loguru import logger + logger.info(rv) m_user_state.permissions = rv @@ -126,13 +146,13 @@ def test_endpoint_get_user_permissions(app): client = TestClient(app) r = client.get("/user/permissions") - assert r.status_code == 200 + assert r.status_code == HTTPStatus.OK response = r.json() assert response.keys() == {"all", "group1"} assert response["all"]["read"] assert response["group1"]["read"] == [] assert response["group1"]["archive_url"] - assert response["all"]["archive_url"] == False + assert response["all"]["archive_url"] is False def test_endpoint_get_user_usage_no_user_auth(client, test_no_auth): @@ -140,8 +160,6 @@ def test_endpoint_get_user_usage_no_user_auth(client, test_no_auth): def test_endpoint_get_user_usage_inactive(app): - from app.web.security import get_user_state - m_user_state = MagicMock() m_user_state.active = False @@ -149,13 +167,11 @@ def test_endpoint_get_user_usage_inactive(app): client = TestClient(app) r = client.get("/user/usage") - assert r.status_code == 403 + assert r.status_code == HTTPStatus.FORBIDDEN assert r.json() == {"detail": "User is not active."} def test_endpoint_get_user_usage_active(app): - from app.web.security import get_user_state - m_user_state = MagicMock() m_user_state.active = True mock_usage = UsageResponse( @@ -164,8 +180,8 @@ def test_endpoint_get_user_usage_active(app): total_sheets=3, groups={ "group1": Usage(monthly_urls=4, monthly_mbs=5, total_sheets=6), - "group2": Usage(monthly_urls=7, monthly_mbs=8, total_sheets=9) - } + "group2": Usage(monthly_urls=7, monthly_mbs=8, total_sheets=9), + }, ) m_user_state.usage.return_value = mock_usage @@ -173,5 +189,5 @@ def test_endpoint_get_user_usage_active(app): client = TestClient(app) r = client.get("/user/usage") - assert r.status_code == 200 + assert r.status_code == HTTPStatus.OK assert UsageResponse(**r.json()) == mock_usage diff --git a/app/tests/web/endpoints/test_interoperability.py b/app/tests/web/endpoints/test_interoperability.py index 703f69a..d102116 100644 --- a/app/tests/web/endpoints/test_interoperability.py +++ b/app/tests/web/endpoints/test_interoperability.py @@ -1,10 +1,9 @@ import json from datetime import datetime +from http import HTTPStatus from unittest.mock import MagicMock, patch from app.shared.db import models -from app.web.config import ALLOW_ANY_EMAIL -from app.web.db import crud def test_submit_manual_archive_unauthenticated(client, test_no_auth): @@ -15,46 +14,134 @@ def test_submit_manual_archive_not_user_auth(client_with_auth, test_no_auth): test_no_auth(client_with_auth.post, "/interop/submit-archive") -@patch("app.web.endpoints.interoperability.business_logic", return_value=MagicMock(get_store_archive_until=MagicMock(return_value=datetime))) +@patch( + "app.web.endpoints.interoperability.business_logic", + return_value=MagicMock( + get_store_archive_until=MagicMock(return_value=datetime) + ), +) def test_submit_manual_archive(m1, client_with_token, db_session): # normal workflow - aa_metadata = json.dumps({"status": "test: success", "metadata": {"url": "http://example.com"}, "media": [{"filename": "fn1", "urls": ["http://example.s3.com"]}]}) - r = client_with_token.post("/interop/submit-archive", json={"result": aa_metadata, "public": True, "author_id": "jerry@gmail.com", "group_id": "spaceship", "tags": ["test"], "url": "http://example.com"}) - assert r.status_code == 201 + aa_metadata = json.dumps( + { + "status": "test: success", + "metadata": {"url": "http://example.com"}, + "media": [{"filename": "fn1", "urls": ["http://example.s3.com"]}], + } + ) + r = client_with_token.post( + "/interop/submit-archive", + json={ + "result": aa_metadata, + "public": True, + "author_id": "jerry@gmail.com", + "group_id": "spaceship", + "tags": ["test"], + "url": "http://example.com", + }, + ) + assert r.status_code == HTTPStatus.CREATED assert "id" in r.json() - inserted = db_session.query(models.Archive).filter(models.Archive.id == r.json()["id"]).first() + inserted = ( + db_session.query(models.Archive) + .filter(models.Archive.id == r.json()["id"]) + .first() + ) assert inserted.url == "http://example.com" assert inserted.group_id == "spaceship" assert inserted.author_id == "jerry@gmail.com" assert sorted([t.id for t in inserted.tags]) == sorted(["test", "manual"]) assert inserted.public - assert type(inserted.result) == dict + assert isinstance(inserted.result, dict) assert [u.url for u in inserted.urls] == ["http://example.s3.com"] - assert type(inserted.store_until) == datetime + assert isinstance(inserted.store_until, datetime) # cannot have the same URL twice - aa_metadata = json.dumps({"status": "test: success", "metadata": {"url": "http://example.com"}, "media": [{"filename": "fn1", "urls": ["http://example.com", "http://example.com"]}]}) - r = client_with_token.post("/interop/submit-archive", json={"result": aa_metadata, "public": False, "author_id": "jerry@gmail.com", "tags": ["test"], "url": "http://example.com"}) - assert r.status_code == 422 - assert r.json() == {"detail": "Cannot insert into DB due to integrity error, likely duplicate urls."} + aa_metadata = json.dumps( + { + "status": "test: success", + "metadata": {"url": "http://example.com"}, + "media": [ + { + "filename": "fn1", + "urls": ["http://example.com", "http://example.com"], + } + ], + } + ) + r = client_with_token.post( + "/interop/submit-archive", + json={ + "result": aa_metadata, + "public": False, + "author_id": "jerry@gmail.com", + "tags": ["test"], + "url": "http://example.com", + }, + ) + assert r.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + assert r.json() == { + "detail": "Cannot insert into DB due to integrity error, likely duplicate urls." + } # test with invalid JSON def test_submit_manual_archive_invalid_json(client_with_token): - r = client_with_token.post("/interop/submit-archive", json={"result": "invalid json", "public": False, "author_id": "jer", "tags": ["test"], "url": "http://example.com"}) - assert r.status_code == 422 + r = client_with_token.post( + "/interop/submit-archive", + json={ + "result": "invalid json", + "public": False, + "author_id": "jer", + "tags": ["test"], + "url": "http://example.com", + }, + ) + assert r.status_code == HTTPStatus.UNPROCESSABLE_ENTITY assert r.json() == {"detail": "Invalid JSON in result field."} -@patch("app.web.endpoints.interoperability.business_logic.get_store_archive_until", side_effect=AssertionError("AssertionError")) -def test_submit_manual_archive_no_store_until(m_sau, client_with_token, db_session): - aa_metadata = json.dumps({"status": "test: success", "metadata": {"url": "http://example.com"}, "media": [{"filename": "fn1", "urls": ["http://example.s3.com"]}]}) - r = client_with_token.post("/interop/submit-archive", json={"result": aa_metadata, "public": True, "author_id": "jerry@gmail.com", "group_id": "spaceship", "tags": ["test"], "url": "http://example.com"}) - assert r.status_code == 201 +@patch( + "app.web.endpoints.interoperability.business_logic.get_store_archive_until", + side_effect=AssertionError("AssertionError"), +) +def test_submit_manual_archive_no_store_until( + m_sau, client_with_token, db_session +): + aa_metadata = json.dumps( + { + "status": "test: success", + "metadata": {"url": "http://example.com"}, + "media": [{"filename": "fn1", "urls": ["http://example.s3.com"]}], + } + ) + r = client_with_token.post( + "/interop/submit-archive", + json={ + "result": aa_metadata, + "public": True, + "author_id": "jerry@gmail.com", + "group_id": "spaceship", + "tags": ["test"], + "url": "http://example.com", + }, + ) + assert r.status_code == HTTPStatus.CREATED assert len(r.json()["id"]) == 36 - res = db_session.query(models.Archive).filter(models.Archive.id == r.json()["id"]).first() + res = ( + db_session.query(models.Archive) + .filter(models.Archive.id == r.json()["id"]) + .first() + ) assert res.store_until is None # testing that store_until = None is not comparable with datetime, and will always return False - res = db_session.query(models.Archive).filter(models.Archive.id == r.json()["id"], models.Archive.store_until < datetime.now()).first() + res = ( + db_session.query(models.Archive) + .filter( + models.Archive.id == r.json()["id"], + models.Archive.store_until < datetime.now(), + ) + .first() + ) assert res is None diff --git a/app/tests/web/endpoints/test_sheet.py b/app/tests/web/endpoints/test_sheet.py index 9b47228..c318496 100644 --- a/app/tests/web/endpoints/test_sheet.py +++ b/app/tests/web/endpoints/test_sheet.py @@ -1,10 +1,13 @@ -import json from datetime import datetime +from http import HTTPStatus from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient +from app.shared.db import models from app.shared.schemas import TaskResult +from app.web.db.user_state import UserState +from app.web.security import get_user_state def test_endpoints_no_auth(client, test_no_auth): @@ -20,34 +23,38 @@ def test_create_sheet_endpoint(app_with_auth, db_session): "id": "123-sheet-id", "name": "Test Sheet", "group_id": "spaceship", - "frequency": "daily" + "frequency": "daily", } # with good data response = client_with_auth.post("/sheet/create", json=good_data) - assert response.status_code == 201 + assert response.status_code == HTTPStatus.CREATED j = response.json() assert datetime.fromisoformat(j.pop("created_at")) assert datetime.fromisoformat(j.pop("last_url_archived_at")) - assert j.pop("author_id") == 'morty@example.com' + assert j.pop("author_id") == "morty@example.com" assert j == good_data # already exists response = client_with_auth.post("/sheet/create", json=good_data) - assert response.status_code == 400 - assert response.json() == {"detail": "Sheet with this ID is already being archived."} + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.json() == { + "detail": "Sheet with this ID is already being archived." + } # bad group bad_data = good_data.copy() bad_data["group_id"] = "not a group" response = client_with_auth.post("/sheet/create", json=bad_data) - assert response.status_code == 403 - assert response.json() == {"detail": "User does not have access to this group."} + assert response.status_code == HTTPStatus.FORBIDDEN + assert response.json() == { + "detail": "User does not have access to this group." + } # switch to jerry who's got less quota/permissions - from app.web.db.user_state import UserState - from app.web.security import get_user_state - app_with_auth.dependency_overrides[get_user_state] = lambda: UserState(db_session, "jerry@example.com") + app_with_auth.dependency_overrides[get_user_state] = lambda: UserState( + db_session, "jerry@example.com" + ) client_jerry = TestClient(app_with_auth) # frequency not allowed @@ -56,39 +63,62 @@ def test_create_sheet_endpoint(app_with_auth, db_session): jerry_data["frequency"] = "hourly" jerry_data["id"] = "jerry-sheet-id" response = client_jerry.post("/sheet/create", json=jerry_data) - assert response.status_code == 422 - assert response.json() == {"detail": "Invalid frequency selected for this group."} + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + assert response.json() == { + "detail": "Invalid frequency selected for this group." + } jerry_data["frequency"] = "daily" # success for the first sheet, bad quota on second response = client_jerry.post("/sheet/create", json=jerry_data) - assert response.status_code == 201 + assert response.status_code == HTTPStatus.CREATED response = client_jerry.post("/sheet/create", json=jerry_data) - assert response.status_code == 429 - assert response.json() == {"detail": "User has reached their sheet quota for this group."} + assert response.status_code == HTTPStatus.TOO_MANY_REQUESTS + assert response.json() == { + "detail": "User has reached their sheet quota for this group." + } def test_get_user_sheets_endpoint(client_with_auth, db_session): # no data response = client_with_auth.get("/sheet/mine") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json() == [] # with data - from app.shared.db import models db_session.add( - models.Sheet(id="123", name="Test Sheet 1", author_id="morty@example.com", group_id="spaceship", frequency="hourly") + models.Sheet( + id="123", + name="Test Sheet 1", + author_id="morty@example.com", + group_id="spaceship", + frequency="hourly", + ) ) db_session.commit() - db_session.add_all([ - models.Sheet(id="456", name="Test Sheet 2", author_id="morty@example.com", group_id="interdimensional", frequency="daily"), - models.Sheet(id="789", name="Test Sheet 3", author_id="rick@example.com", group_id="interdimensional", frequency="hourly"), - ]) + db_session.add_all( + [ + models.Sheet( + id="456", + name="Test Sheet 2", + author_id="morty@example.com", + group_id="interdimensional", + frequency="daily", + ), + models.Sheet( + id="789", + name="Test Sheet 3", + author_id="rick@example.com", + group_id="interdimensional", + frequency="hourly", + ), + ] + ) db_session.commit() response = client_with_auth.get("/sheet/mine") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK r = response.json() assert isinstance(r, list) assert len(r) == 2 @@ -97,65 +127,84 @@ def test_get_user_sheets_endpoint(client_with_auth, db_session): assert datetime.fromisoformat(r[1].pop("created_at")) assert datetime.fromisoformat(r[1].pop("last_url_archived_at")) assert r[0] == { - 'id': '123', - 'author_id': 'morty@example.com', - 'frequency': 'hourly', - 'group_id': 'spaceship', - 'name': 'Test Sheet 1', + "id": "123", + "author_id": "morty@example.com", + "frequency": "hourly", + "group_id": "spaceship", + "name": "Test Sheet 1", } assert r[1] == { - 'id': '456', - 'author_id': 'morty@example.com', - 'frequency': 'daily', - 'group_id': 'interdimensional', - 'name': 'Test Sheet 2', + "id": "456", + "author_id": "morty@example.com", + "frequency": "daily", + "group_id": "interdimensional", + "name": "Test Sheet 2", } def test_delete_sheet_endpoint(client_with_auth, db_session): # missing sheet response = client_with_auth.delete("/sheet/123-sheet-id") - assert response.status_code == 200 - assert response.json() == { - "id": "123-sheet-id", - "deleted": False - } + assert response.status_code == HTTPStatus.OK + assert response.json() == {"id": "123-sheet-id", "deleted": False} # add sheets for deletion - from app.shared.db import models - db_session.add_all([ - models.Sheet(id="123-sheet-id", name="Test Sheet 1", author_id="morty@example.com", group_id="interdimensional", frequency="daily"), - models.Sheet(id="456-sheet-id", name="Test Sheet 2", author_id="rick@example.com", group_id="spaceship", frequency="hourly"), - ]) + db_session.add_all( + [ + models.Sheet( + id="123-sheet-id", + name="Test Sheet 1", + author_id="morty@example.com", + group_id="interdimensional", + frequency="daily", + ), + models.Sheet( + id="456-sheet-id", + name="Test Sheet 2", + author_id="rick@example.com", + group_id="spaceship", + frequency="hourly", + ), + ] + ) db_session.commit() # morty can delete his response = client_with_auth.delete("/sheet/123-sheet-id") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json() == {"id": "123-sheet-id", "deleted": True} # but only once response = client_with_auth.delete("/sheet/123-sheet-id") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json() == {"id": "123-sheet-id", "deleted": False} - # and not rick's + # and not Rick's response = client_with_auth.delete("/sheet/456-sheet-id") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json() == {"id": "456-sheet-id", "deleted": False} class TestArchiveUserSheetEndpoint: @patch("app.web.endpoints.sheet.celery", return_value=MagicMock()) def test_normal_flow(self, m_celery, client_with_auth, db_session): - from app.shared.db import models - db_session.add(models.Sheet(id="123-sheet-id", name="Test Sheet 1", author_id="morty@example.com", group_id="spaceship", frequency="hourly")) + db_session.add( + models.Sheet( + id="123-sheet-id", + name="Test Sheet 1", + author_id="morty@example.com", + group_id="spaceship", + frequency="hourly", + ) + ) db_session.commit() m_signature = MagicMock() - m_signature.apply_async.return_value = TaskResult(id="123-taskid", status="PENDING", result="") + m_signature.apply_async.return_value = TaskResult( + id="123-taskid", status="PENDING", result="" + ) m_celery.signature.return_value = m_signature r = client_with_auth.post("/sheet/123-sheet-id/archive") - assert r.status_code == 201 + assert r.status_code == HTTPStatus.CREATED assert r.json() == {"id": "123-taskid"} m_celery.signature.assert_called_once() m_signature.apply_async.assert_called_once() @@ -165,29 +214,54 @@ class TestArchiveUserSheetEndpoint: def test_missing_data(self, client_with_auth): r = client_with_auth.post("/sheet/123-sheet-id/archive") - assert r.status_code == 403 + assert r.status_code == HTTPStatus.FORBIDDEN assert r.json() == {"detail": "No access to this sheet."} def test_no_access(self, client_with_auth, db_session): - from app.shared.db import models - db_session.add(models.Sheet(id="123-sheet-id", name="Test Sheet 1", author_id="rick@example.com", group_id="spaceship", frequency="hourly")) + db_session.add( + models.Sheet( + id="123-sheet-id", + name="Test Sheet 1", + author_id="rick@example.com", + group_id="spaceship", + frequency="hourly", + ) + ) db_session.commit() r = client_with_auth.post("/sheet/123-sheet-id/archive") - assert r.status_code == 403 + assert r.status_code == HTTPStatus.FORBIDDEN assert r.json() == {"detail": "No access to this sheet."} def test_user_not_in_group(self, client_with_auth, db_session): - from app.shared.db import models - db_session.add(models.Sheet(id="123-sheet-id", name="Test Sheet 1", author_id="morty@example.com", group_id="interdimensional", frequency="hourly")) + db_session.add( + models.Sheet( + id="123-sheet-id", + name="Test Sheet 1", + author_id="morty@example.com", + group_id="interdimensional", + frequency="hourly", + ) + ) db_session.commit() r = client_with_auth.post("/sheet/123-sheet-id/archive") - assert r.status_code == 403 - assert r.json() == {"detail": "User does not have access to this group."} + assert r.status_code == HTTPStatus.FORBIDDEN + assert r.json() == { + "detail": "User does not have access to this group." + } def test_user_cannot_manually_trigger(self, client_with_auth, db_session): - from app.shared.db import models - db_session.add(models.Sheet(id="123-sheet-id", name="Test Sheet 1", author_id="morty@example.com", group_id="default", frequency="hourly")) + db_session.add( + models.Sheet( + id="123-sheet-id", + name="Test Sheet 1", + author_id="morty@example.com", + group_id="default", + frequency="hourly", + ) + ) db_session.commit() r = client_with_auth.post("/sheet/123-sheet-id/archive") - assert r.status_code == 429 - assert r.json() == {"detail": "User cannot manually trigger sheet archiving in this group."} + assert r.status_code == HTTPStatus.TOO_MANY_REQUESTS + assert r.json() == { + "detail": "User cannot manually trigger sheet archiving in this group." + } diff --git a/app/tests/web/endpoints/test_task.py b/app/tests/web/endpoints/test_task.py index 937ad46..038babf 100644 --- a/app/tests/web/endpoints/test_task.py +++ b/app/tests/web/endpoints/test_task.py @@ -1,3 +1,4 @@ +from http import HTTPStatus from unittest.mock import patch @@ -12,27 +13,26 @@ def test_get_status_success(mock_async_result, client_with_auth): response = client_with_auth.get("/task/test-task-id") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json() == { "id": "test-task-id", "status": "SUCCESS", - "result": {"data": "some result"} + "result": {"data": "some result"}, } @patch("app.web.endpoints.task.AsyncResult") def test_get_status_failure(mock_async_result, client_with_auth): - mock_async_result.return_value.status = "FAILURE" mock_async_result.return_value.result = Exception("Some error") response = client_with_auth.get("/task/test-task-id") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json() == { "id": "test-task-id", "status": "FAILURE", - "result": {"error": "Some error"} + "result": {"error": "Some error"}, } @@ -43,9 +43,9 @@ def test_get_status_pending(mock_async_result, client_with_auth): response = client_with_auth.get("/task/test-task-id") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json() == { "id": "test-task-id", "status": "PENDING", - "result": None + "result": None, } diff --git a/app/tests/web/endpoints/test_url.py b/app/tests/web/endpoints/test_url.py index 1b6ee85..cd64262 100644 --- a/app/tests/web/endpoints/test_url.py +++ b/app/tests/web/endpoints/test_url.py @@ -1,6 +1,9 @@ import json +from http import HTTPStatus from unittest.mock import MagicMock, patch +from app.shared import schemas +from app.shared.db import worker_crud from app.shared.schemas import ArchiveCreate, TaskResult from app.web.config import ALLOW_ANY_EMAIL @@ -13,7 +16,9 @@ def test_archive_url_unauthenticated(client, test_no_auth): @patch("app.web.endpoints.url.celery", return_value=MagicMock()) def test_archive_url(m_celery, m2, client_with_auth): m_signature = MagicMock() - m_signature.apply_async.return_value = TaskResult(id="123-456-789", status="PENDING", result="") + m_signature.apply_async.return_value = TaskResult( + id="123-456-789", status="PENDING", result="" + ) m_celery.signature.return_value = m_signature m_user_state = MagicMock() @@ -21,62 +26,98 @@ def test_archive_url(m_celery, m2, client_with_auth): # url is too short response = client_with_auth.post("/url/archive", json={"url": "bad"}) - assert response.status_code == 422 - assert response.json()["detail"][0]["msg"] == 'String should have at least 5 characters' + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY + assert ( + response.json()["detail"][0]["msg"] + == "String should have at least 5 characters" + ) m_celery.signature.assert_not_called() # url is invalid - response = client_with_auth.post("/url/archive", json={"url": "example.com"}) - assert response.status_code == 400 + response = client_with_auth.post( + "/url/archive", json={"url": "example.com"} + ) + assert response.status_code == HTTPStatus.BAD_REQUEST assert response.json()["detail"] == "Invalid URL received." # valid request m_user_state.has_quota_max_monthly_urls.return_value = True m_user_state.has_quota_max_monthly_mbs.return_value = True - response = client_with_auth.post("/url/archive", json={"url": "https://example.com"}) - assert response.status_code == 201 - assert response.json() == {'id': '123-456-789'} + response = client_with_auth.post( + "/url/archive", json={"url": "https://example.com"} + ) + assert response.status_code == HTTPStatus.CREATED + assert response.json() == {"id": "123-456-789"} m_celery.signature.assert_called_once() m_signature.apply_async.assert_called_once() called_val = m_celery.signature.call_args assert called_val[0][0] == "create_archive_task" - assert json.loads(called_val[1]['args'][0]) == {"id": None, "url": "https://example.com", "result": None, "public": False, "author_id": "rick@example.com", "group_id": "default", "tags": None, "sheet_id": None, "store_until": None, "urls": None} + assert json.loads(called_val[1]["args"][0]) == { + "id": None, + "url": "https://example.com", + "result": None, + "public": False, + "author_id": "rick@example.com", + "group_id": "default", + "tags": None, + "sheet_id": None, + "store_until": None, + "urls": None, + } m_user_state.has_quota_max_monthly_urls.assert_called_once() m_user_state.has_quota_max_monthly_mbs.assert_called_once() m_user_state.in_group.assert_called_once_with("default") # user is not in group m_user_state.in_group.return_value = False - response = client_with_auth.post("/url/archive", json={"url": "https://example.com", "group_id": "new-group"}) - assert response.status_code == 403 - assert response.json()["detail"] == "User does not have access to this group." + response = client_with_auth.post( + "/url/archive", + json={"url": "https://example.com", "group_id": "new-group"}, + ) + assert response.status_code == HTTPStatus.FORBIDDEN + assert ( + response.json()["detail"] == "User does not have access to this group." + ) m_user_state.in_group.assert_called_with("new-group") # user is in group m_user_state.in_group.return_value = True - response = client_with_auth.post("/url/archive", json={"url": "https://example.com", "group_id": "spaceship"}) - assert response.status_code == 201 - assert response.json() == {'id': '123-456-789'} + response = client_with_auth.post( + "/url/archive", + json={"url": "https://example.com", "group_id": "spaceship"}, + ) + assert response.status_code == HTTPStatus.CREATED + assert response.json() == {"id": "123-456-789"} assert m_celery.signature.call_count == 2 assert m_signature.apply_async.call_count == 2 called_val = m_celery.signature.call_args - assert json.loads(called_val[1]['args'][0])["group_id"] == "spaceship" + assert json.loads(called_val[1]["args"][0])["group_id"] == "spaceship" m_user_state.in_group.assert_called_with("spaceship") # user is over monthly URL quota m_user_state.has_quota_max_monthly_urls.return_value = False m_user_state.has_quota_max_monthly_mbs.return_value = True - response = client_with_auth.post("/url/archive", json={"url": "https://example.com", "group_id": "spaceship"}) - assert response.status_code == 429 - assert response.json()["detail"] == "User has reached their monthly URL quota." + response = client_with_auth.post( + "/url/archive", + json={"url": "https://example.com", "group_id": "spaceship"}, + ) + assert response.status_code == HTTPStatus.TOO_MANY_REQUESTS + assert ( + response.json()["detail"] == "User has reached their monthly URL quota." + ) m_user_state.has_quota_max_monthly_urls.assert_called_with("spaceship") # user is over monthly MB quota m_user_state.has_quota_max_monthly_urls.return_value = True m_user_state.has_quota_max_monthly_mbs.return_value = False - response = client_with_auth.post("/url/archive", json={"url": "https://example.com", "group_id": "spacesuit"}) - assert response.status_code == 429 - assert response.json()["detail"] == "User has reached their monthly MB quota." + response = client_with_auth.post( + "/url/archive", + json={"url": "https://example.com", "group_id": "spacesuit"}, + ) + assert response.status_code == HTTPStatus.TOO_MANY_REQUESTS + assert ( + response.json()["detail"] == "User has reached their monthly MB quota." + ) m_user_state.has_quota_max_monthly_mbs.assert_called_with("spacesuit") assert m_celery.signature.call_count == 2 assert m_signature.apply_async.call_count == 2 @@ -89,40 +130,77 @@ def test_archive_url_quotas(m1, client_with_auth): # misses on monthly URLs quota m_user_state.has_quota_max_monthly_urls.return_value = False - response = client_with_auth.post("/url/archive", json={"url": "https://example.com"}) - assert response.status_code == 429 - assert response.json()["detail"] == "User has reached their monthly URL quota." + response = client_with_auth.post( + "/url/archive", json={"url": "https://example.com"} + ) + assert response.status_code == HTTPStatus.TOO_MANY_REQUESTS + assert ( + response.json()["detail"] == "User has reached their monthly URL quota." + ) m_user_state.has_quota_max_monthly_urls.assert_called_once() # misses on monthly MBs quota m_user_state.has_quota_max_monthly_urls.return_value = True m_user_state.has_quota_max_monthly_mbs.return_value = False - response = client_with_auth.post("/url/archive", json={"url": "https://example.com"}) - assert response.status_code == 429 - assert response.json()["detail"] == "User has reached their monthly MB quota." + response = client_with_auth.post( + "/url/archive", json={"url": "https://example.com"} + ) + assert response.status_code == HTTPStatus.TOO_MANY_REQUESTS + assert ( + response.json()["detail"] == "User has reached their monthly MB quota." + ) m_user_state.has_quota_max_monthly_mbs.assert_called_once() @patch("app.web.endpoints.url.celery", return_value=MagicMock()) def test_archive_url_with_api_token(m_celery, client_with_token): m_signature = MagicMock() - m_signature.apply_async.return_value = TaskResult(id="123-456-789", status="PENDING", result="") + m_signature.apply_async.return_value = TaskResult( + id="123-456-789", status="PENDING", result="" + ) m_celery.signature.return_value = m_signature - response = client_with_token.post("/url/archive", json={"url": "https://example.com", "author_id": "someone@example.com"}) - assert response.status_code == 201 - assert response.json() == {'id': '123-456-789'} + response = client_with_token.post( + "/url/archive", + json={"url": "https://example.com", "author_id": "someone@example.com"}, + ) + assert response.status_code == HTTPStatus.CREATED + assert response.json() == {"id": "123-456-789"} m_celery.signature.assert_called_once() m_signature.apply_async.assert_called_once() called_val = m_celery.signature.call_args assert called_val[0][0] == "create_archive_task" - assert json.loads(called_val[1]['args'][0]) == {"id": None, "url": "https://example.com", "result": None, "public": False, "author_id": "someone@example.com", "group_id": "default", "tags": None, "sheet_id": None, "store_until": None, "urls": None} + assert json.loads(called_val[1]["args"][0]) == { + "id": None, + "url": "https://example.com", + "result": None, + "public": False, + "author_id": "someone@example.com", + "group_id": "default", + "tags": None, + "sheet_id": None, + "store_until": None, + "urls": None, + } # missing id should use ALLOW_ANY_EMAIL - response = client_with_token.post("/url/archive", json={"url": "https://example.com", "author_id": None}) - assert response.status_code == 201 + response = client_with_token.post( + "/url/archive", json={"url": "https://example.com", "author_id": None} + ) + assert response.status_code == HTTPStatus.CREATED called_val = m_celery.signature.call_args assert called_val[0][0] == "create_archive_task" - assert json.loads(called_val[1]['args'][0]) == {"id": None, "url": "https://example.com", "result": None, "public": False, "author_id": ALLOW_ANY_EMAIL, "group_id": "default", "tags": None, "sheet_id": None, "store_until": None, "urls": None} + assert json.loads(called_val[1]["args"][0]) == { + "id": None, + "url": "https://example.com", + "result": None, + "public": False, + "author_id": ALLOW_ANY_EMAIL, + "group_id": "default", + "tags": None, + "sheet_id": None, + "store_until": None, + "urls": None, + } def test_search_by_url_unauthenticated(client, test_no_auth): @@ -132,46 +210,67 @@ def test_search_by_url_unauthenticated(client, test_no_auth): def test_search_by_url(client_with_auth, client_with_token, db_session): # tests the search endpoint, including through some db data for the endpoint params response = client_with_auth.get("/url/search") - assert response.status_code == 422 + assert response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY assert response.json()["detail"][0]["msg"] == "Field required" response = client_with_auth.get("/url/search?url=https://example.com") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json() == [] - from app.shared import schemas - from app.shared.db import worker_crud for i in range(11): - worker_crud.create_archive(db_session, ArchiveCreate(id=f"url-456-{i}", url="https://example.com" if i < 10 else "https://something-else.com", result={}, public=True, author_id="rick@example.com"), [], []) + worker_crud.create_archive( + db_session, + ArchiveCreate( + id=f"url-456-{i}", + url="https://example.com" + if i < 10 + else "https://something-else.com", + result={}, + public=True, + author_id="rick@example.com", + ), + [], + [], + ) # NB: this insertion is too fast for the ordering to be correct as they are within the same second response = client_with_auth.get("/url/search?url=https://example.com") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert len(j := response.json()) == 10 assert "url-456-0" in [i["id"] for i in j] assert "url-456-9" in [i["id"] for i in j] assert "url-456-10" not in [i["id"] for i in j] assert j[0].keys() == schemas.ArchiveResult.model_fields.keys() - response = client_with_auth.get("/url/search?url=https://example.com&limit=5") - assert response.status_code == 200 + response = client_with_auth.get( + "/url/search?url=https://example.com&limit=5" + ) + assert response.status_code == HTTPStatus.OK assert len(response.json()) == 5 - response = client_with_auth.get("/url/search?url=https://example.com&skip=5&limit=2") - assert response.status_code == 200 + response = client_with_auth.get( + "/url/search?url=https://example.com&skip=5&limit=2" + ) + assert response.status_code == HTTPStatus.OK assert len(response.json()) == 2 - response = client_with_auth.get("/url/search?url=https://example.com&archived_before=2010-01-01") - assert response.status_code == 200 + response = client_with_auth.get( + "/url/search?url=https://example.com&archived_before=2010-01-01" + ) + assert response.status_code == HTTPStatus.OK assert len(response.json()) == 0 - response = client_with_auth.get("/url/search?url=https://example.com&archived_after=2010-01-01") - assert response.status_code == 200 + response = client_with_auth.get( + "/url/search?url=https://example.com&archived_after=2010-01-01" + ) + assert response.status_code == HTTPStatus.OK assert len(response.json()) == 10 # API token will also work - response = client_with_token.get("/url/search?url=https://example.com&archived_after=2010-01-01") - assert response.status_code == 200 + response = client_with_token.get( + "/url/search?url=https://example.com&archived_after=2010-01-01" + ) + assert response.status_code == HTTPStatus.OK assert len(response.json()) == 10 @@ -181,7 +280,7 @@ def test_search_no_read_access(mock_user_state, client_with_auth): mock_user_state.return_value.read_public = False response = client_with_auth.get("/url/search?url=https://example.com") - assert response.status_code == 403 + assert response.status_code == HTTPStatus.FORBIDDEN assert response.json() == {"detail": "User does not have read access."} @@ -191,12 +290,22 @@ def test_delete_task_unauthenticated(client, test_no_auth): def test_delete_task(client_with_auth, db_session): response = client_with_auth.delete("/url/delete-123-456-789") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json() == {"id": "delete-123-456-789", "deleted": False} - from app.shared.db import worker_crud - worker_crud.create_archive(db_session, ArchiveCreate(id="delete-123-456-789", url="https://example.com", result={}, public=True, author_id="morty@example.com"), [], []) + worker_crud.create_archive( + db_session, + ArchiveCreate( + id="delete-123-456-789", + url="https://example.com", + result={}, + public=True, + author_id="morty@example.com", + ), + [], + [], + ) response = client_with_auth.delete("/url/delete-123-456-789") - assert response.status_code == 200 + assert response.status_code == HTTPStatus.OK assert response.json() == {"id": "delete-123-456-789", "deleted": True} diff --git a/app/tests/web/test_main.py b/app/tests/web/test_main.py index a4ddf1e..0dbd5fa 100644 --- a/app/tests/web/test_main.py +++ b/app/tests/web/test_main.py @@ -1,25 +1,32 @@ import os import shutil +from http import HTTPStatus from unittest.mock import patch +import alembic.config import pytest from fastapi.testclient import TestClient +from app.web.utils.metrics import EXCEPTION_COUNTER + def test_lifespan(app): with TestClient(app) as client: r = client.get("/health") - assert r.status_code == 200 + assert r.status_code == HTTPStatus.OK assert r.json() == {"status": "ok"} -def test_alembic(db_session): - import alembic.config - alembic.config.main(argv=['--raiseerr', 'upgrade', 'head']) - alembic.config.main(argv=['--raiseerr', 'downgrade', 'base']) -@patch("app.web.endpoints.url.crud.soft_delete_archive", side_effect=Exception('mocked error')) +def test_alembic(db_session): + alembic.config.main(argv=["--raiseerr", "upgrade", "head"]) + alembic.config.main(argv=["--raiseerr", "downgrade", "base"]) + + +@patch( + "app.web.endpoints.url.crud.soft_delete_archive", + side_effect=Exception("mocked error"), +) def test_logging_middleware(m1, client_with_auth): - from app.web.utils.metrics import EXCEPTION_COUNTER assert len(EXCEPTION_COUNTER.collect()[0].samples) == 0 with pytest.raises(Exception, match="mocked error"): client_with_auth.delete("/url/123") @@ -37,12 +44,13 @@ def test_serve_local_archive_logic(get_settings): # modify the settings get_settings.SERVE_LOCAL_ARCHIVE = "/app/local_archive_test" from app.web.main import app_factory + app = app_factory(get_settings) # test client = TestClient(app) r = client.get("/app/local_archive_test/temp.txt") - assert r.status_code == 200 + assert r.status_code == HTTPStatus.OK assert r.text == "test" finally: # cleanup diff --git a/app/tests/web/test_security.py b/app/tests/web/test_security.py index 55a434b..07943e5 100644 --- a/app/tests/web/test_security.py +++ b/app/tests/web/test_security.py @@ -1,3 +1,4 @@ +from http import HTTPStatus from unittest.mock import Mock, patch import pytest @@ -5,112 +6,177 @@ from fastapi import HTTPException from fastapi.security import HTTPAuthorizationCredentials from app.web.config import ALLOW_ANY_EMAIL +from app.web.db.user_state import UserState +from app.web.security import ( + authenticate_user, + get_token_or_user_auth, + get_user_auth, + get_user_state, + secure_compare, + token_api_key_auth, +) def test_secure_compare(): - from app.web.security import secure_compare - assert secure_compare("test", "test") assert not secure_compare("test", "test2") @pytest.mark.asyncio async def test_get_token_or_user_auth_with_api(): - from app.web.security import get_token_or_user_auth - mock_api = HTTPAuthorizationCredentials(scheme="lorem", credentials="this_is_the_test_api_token") + mock_api = HTTPAuthorizationCredentials( + scheme="lorem", credentials="this_is_the_test_api_token" + ) assert await get_token_or_user_auth(mock_api) == ALLOW_ANY_EMAIL @pytest.mark.asyncio async def test_get_token_or_user_auth_with_user(): - from app.web.security import get_token_or_user_auth - bad_user = HTTPAuthorizationCredentials(scheme="ipsum", credentials="invalid") - e: pytest.ExceptionInfo = None + bad_user = HTTPAuthorizationCredentials( + scheme="ipsum", credentials="invalid" + ) with pytest.raises(HTTPException) as e: await get_token_or_user_auth(bad_user) - assert e.value.status_code == 401 + assert e.value.status_code == HTTPStatus.UNAUTHORIZED assert e.value.detail == "invalid access_token" -@patch("app.web.security.authenticate_user", return_value=(True, "summer@example.com")) +@patch( + "app.web.security.authenticate_user", + return_value=(True, "summer@example.com"), +) @pytest.mark.asyncio async def test_get_user_auth(m1): - from app.web.security import get_user_auth - good_user = HTTPAuthorizationCredentials(scheme="ipsum", credentials="valid-and-good") + good_user = HTTPAuthorizationCredentials( + scheme="ipsum", credentials="valid-and-good" + ) assert await get_user_auth(good_user) == "summer@example.com" @patch("app.web.security.secure_compare", return_value=False) @pytest.mark.asyncio async def test_token_api_key_auth_exception(m1): - from app.web.security import token_api_key_auth - - e: pytest.ExceptionInfo = None with pytest.raises(HTTPException) as e: - await token_api_key_auth(HTTPAuthorizationCredentials(scheme="ipsum", credentials="does-not-matter"), auto_error=True) - assert e.value.status_code == 401 + await token_api_key_auth( + HTTPAuthorizationCredentials( + scheme="ipsum", credentials="does-not-matter" + ), + auto_error=True, + ) + assert e.value.status_code == HTTPStatus.UNAUTHORIZED assert e.value.detail == "Wrong auth credentials" @pytest.mark.asyncio async def test_authenticate_user(): - from app.web.security import authenticate_user - assert authenticate_user("test") == (False, "invalid access_token") assert authenticate_user(123) == (False, "invalid access_token") with patch("app.web.security.requests.get") as mock_get: # bad response from oauth2 - mock_get.return_value.status_code = 403 - assert authenticate_user("this-will-call-requests") == (False, "invalid token") + mock_get.return_value.status_code = HTTPStatus.FORBIDDEN + assert authenticate_user("this-will-call-requests") == ( + False, + "invalid token", + ) assert mock_get.call_count == 1 # 200 but invalid json - mock_get.return_value.status_code = 200 - assert authenticate_user("this-will-call-requests") == (False, "token does not belong to valid APP_ID") + mock_get.return_value.status_code = HTTPStatus.OK + assert authenticate_user("this-will-call-requests") == ( + False, + "token does not belong to valid APP_ID", + ) assert mock_get.call_count == 2 # 200 but invalid azp and aud - mock_get.return_value.json.return_value = {"email": "summer@example.com", "azp": "not_an_app"} - assert authenticate_user("this-will-call-requests") == (False, "token does not belong to valid APP_ID") + mock_get.return_value.json.return_value = { + "email": "summer@example.com", + "azp": "not_an_app", + } + assert authenticate_user("this-will-call-requests") == ( + False, + "token does not belong to valid APP_ID", + ) - mock_get.return_value.json.return_value = {"email": "summer@example.com", "aud": "not_an_app"} - assert authenticate_user("this-will-call-requests") == (False, "token does not belong to valid APP_ID") + mock_get.return_value.json.return_value = { + "email": "summer@example.com", + "aud": "not_an_app", + } + assert authenticate_user("this-will-call-requests") == ( + False, + "token does not belong to valid APP_ID", + ) - mock_get.return_value.json.return_value = {"email": "summer@example.com", "azp": "not_an_app", "aud": "not_an_app"} - assert authenticate_user("this-will-call-requests") == (False, "token does not belong to valid APP_ID") + mock_get.return_value.json.return_value = { + "email": "summer@example.com", + "azp": "not_an_app", + "aud": "not_an_app", + } + assert authenticate_user("this-will-call-requests") == ( + False, + "token does not belong to valid APP_ID", + ) # blocked email - mock_get.return_value.json.return_value = {"email": "blocked@example.com", "azp": "test_app_id_1", "aud": "not_an_app"} - assert authenticate_user("this-will-call-requests") == (False, "email 'blocked@example.com' not allowed") + mock_get.return_value.json.return_value = { + "email": "blocked@example.com", + "azp": "test_app_id_1", + "aud": "not_an_app", + } + assert authenticate_user("this-will-call-requests") == ( + False, + "email 'blocked@example.com' not allowed", + ) # not verified - mock_get.return_value.json.return_value = {"email": "summer@example.com", "azp": "not_an_app", "aud": "test_app_id_1"} - assert authenticate_user("this-will-call-requests") == (False, "email 'summer@example.com' not verified") + mock_get.return_value.json.return_value = { + "email": "summer@example.com", + "azp": "not_an_app", + "aud": "test_app_id_1", + } + assert authenticate_user("this-will-call-requests") == ( + False, + "email 'summer@example.com' not verified", + ) # token expired - mock_get.return_value.json.return_value = {"email": "summer@example.com", "azp": "test_app_id_2", "email_verified": "true"} - assert authenticate_user("this-will-call-requests") == (False, "Token expired") + mock_get.return_value.json.return_value = { + "email": "summer@example.com", + "azp": "test_app_id_2", + "email_verified": "true", + } + assert authenticate_user("this-will-call-requests") == ( + False, + "Token expired", + ) # 200 and valid azp and aup and verified - mock_get.return_value.json.return_value = {"email": "summer@example.com", "azp": "test_app_id_2", "email_verified": "true", "expires_in": 100} - assert authenticate_user("this-will-call-requests") == (True, "summer@example.com") + mock_get.return_value.json.return_value = { + "email": "summer@example.com", + "azp": "test_app_id_2", + "email_verified": "true", + "expires_in": 100, + } + assert authenticate_user("this-will-call-requests") == ( + True, + "summer@example.com", + ) assert mock_get.call_count == 9 @pytest.mark.asyncio async def test_authenticate_user_exception(): - from app.web.security import authenticate_user with patch("app.web.security.requests.get") as mock_get: - mock_get.return_value.status_code = 200 + mock_get.return_value.status_code = HTTPStatus.OK mock_get.return_value.json.side_effect = Exception("mocked error") - assert authenticate_user("this-will-call-requests") == (False, "exception occurred") + assert authenticate_user("this-will-call-requests") == ( + False, + "exception occurred", + ) def test_get_user_state(): - from app.web.db.user_state import UserState - from app.web.security import get_user_state - mock_session = Mock() test_email = "test@example.com" diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py index 9a77528..e67aa88 100644 --- a/app/tests/worker/test_worker_main.py +++ b/app/tests/worker/test_worker_main.py @@ -6,23 +6,41 @@ from auto_archiver.core import Media, Metadata from app.shared import schemas from app.shared.db import models +from app.worker.main import create_archive_task, create_sheet_task, get_all_urls -class Test_create_archive_task(): +class TestCreateArchiveTask: URL = "https://example-live.com" - archive = schemas.ArchiveCreate(url=URL, tags=["tag-celery"], public=True, author_id="rick@example.com", group_id="interstellar") + archive = schemas.ArchiveCreate( + url=URL, + tags=["tag-celery"], + public=True, + author_id="rick@example.com", + group_id="interstellar", + ) @patch("app.worker.main.ArchivingOrchestrator") @patch("app.worker.main.get_all_urls", return_value=[]) @patch("app.worker.main.insert_result_into_db") @patch("app.worker.main.get_store_until", return_value=datetime.now()) - @patch("app.worker.main.get_orchestrator_args", return_value=["arg1", "arg2"]) + @patch( + "app.worker.main.get_orchestrator_args", return_value=["arg1", "arg2"] + ) @patch("celery.app.task.Task.request") - def test_success(self, m_req, m_args, m_store, m_insert, m_urls, m_orchestrator, db_session): - from app.worker.main import create_archive_task - + def test_success( + self, + m_req, + m_args, + m_store, + m_insert, + m_urls, + m_orchestrator, + db_session, + ): m_req.id = "this-just-in" - m_orchestrator.return_value.feed.return_value = iter([Metadata().set_url(self.URL).success()]) + m_orchestrator.return_value.feed.return_value = iter( + [Metadata().set_url(self.URL).success()] + ) task = create_archive_task(self.archive.model_dump_json()) @@ -38,15 +56,15 @@ class Test_create_archive_task(): assert len(task["media"]) == 0 def test_raise_invalid(self): - from app.worker.main import create_archive_task - with pytest.raises(Exception): + with pytest.raises(Exception) as _: create_archive_task(self.archive.model_dump_json()) @patch("app.worker.main.ArchivingOrchestrator") @patch("app.worker.main.get_orchestrator_args") def test_raise_db_error(self, m_args, m_orchestrator): - from app.worker.main import create_archive_task - m_orchestrator.return_value.feed.side_effect = Exception("Orchestrator failed") + m_orchestrator.return_value.feed.side_effect = Exception( + "Orchestrator failed" + ) with pytest.raises(Exception) as e: create_archive_task(self.archive.model_dump_json()) @@ -58,7 +76,6 @@ class Test_create_archive_task(): @patch("app.worker.main.insert_result_into_db", return_value=None) @patch("app.worker.main.get_orchestrator_args") def test_raise_empty_result(self, m_args, m_insert, m_orchestrator): - from app.worker.main import create_archive_task m_orchestrator.return_value.feed.return_value = iter([None]) with pytest.raises(Exception) as e: @@ -67,61 +84,83 @@ class Test_create_archive_task(): m_orchestrator.return_value.feed.assert_called_once() -class Test_create_sheet_task(): +class TestCreateSheetTask: URL = "https://example-live.com" - sheet = schemas.SubmitSheet(sheet_id="123", author_id="rick@example.com", group_id="interstellar", tags=["spaceship"]) + sheet = schemas.SubmitSheet( + sheet_id="123", + author_id="rick@example.com", + group_id="interstellar", + tags=["spaceship"], + ) @patch("app.worker.main.get_all_urls", return_value=[]) @patch("app.worker.main.ArchivingOrchestrator") @patch("app.worker.main.models.generate_uuid", return_value="constant-uuid") @patch("app.worker.main.get_store_until", return_value=datetime.now()) @patch("app.worker.main.get_orchestrator_args") - def test_success(self, m_args, m_store, m_uuid, m_orchestrator, m_urls, db_session): - from app.worker.main import create_sheet_task - - assert db_session.query(models.Archive).filter(models.Archive.url == self.URL).count() == 0 + def test_success( + self, m_args, m_store, m_uuid, m_orchestrator, m_urls, db_session + ): + assert ( + db_session.query(models.Archive) + .filter(models.Archive.url == self.URL) + .count() + == 0 + ) mock_metadata = Metadata().set_url(self.URL).success() mock_metadata.add_media(Media("fn1.txt", urls=["outcome1.com"])) - m_orchestrator.return_value.feed.return_value = iter([False, mock_metadata, mock_metadata]) + m_orchestrator.return_value.feed.return_value = iter( + [False, mock_metadata, mock_metadata] + ) res = create_sheet_task(self.sheet.model_dump_json()) - m_args.assert_called_once_with("interstellar", True, ["--gsheet_feeder.sheet_id", "123"]) + m_args.assert_called_once_with( + "interstellar", True, ["--gsheet_feeder.sheet_id", "123"] + ) m_orchestrator.return_value.setup.assert_called_once() m_orchestrator.return_value.feed.assert_called_once() m_store.assert_called_with("interstellar") - m_store.call_count == 2 - m_uuid.call_count == 2 - assert type(res) == dict + assert m_store.call_count == 2 + assert m_uuid.call_count == 2 + assert isinstance(res, dict) assert res["stats"]["archived"] == 1 assert res["stats"]["failed"] == 1 assert len(res["stats"]["errors"]) == 1 assert res["sheet_id"] == "123" assert res["success"] - assert type(res["time"]) == datetime + assert isinstance(res["time"], datetime) # query created archive entry - inserted = db_session.query(models.Archive).filter(models.Archive.url == self.URL).one() + inserted = ( + db_session.query(models.Archive) + .filter(models.Archive.url == self.URL) + .one() + ) assert inserted is not None assert inserted.url == self.URL assert len(inserted.tags) == 1 assert inserted.tags[0].id == "spaceship" assert inserted.group_id == "interstellar" assert inserted.author_id == "rick@example.com" - assert inserted.public == False + assert inserted.public is False def test_get_all_urls(db_session): - from app.worker.main import get_all_urls - meta = Metadata().set_url("https://example.com") m1 = meta.add_media(Media("fn1.txt", urls=["outcome1.com"])) m2 = meta.add_media(Media("fn2.txt", urls=["outcome2.com"])) m3 = meta.add_media(Media("fn3.txt", urls=["outcome3.com"])) m1.set("screenshot", Media("screenshot.png", urls=["screenshot.com"])) - m2.set("thumbnails", [Media("thumb1.png", urls=["thumb1.com"]), Media("thumb2.png", urls=["thumb2.com"])]) + m2.set( + "thumbnails", + [ + Media("thumb1.png", urls=["thumb1.com"]), + Media("thumb2.png", urls=["thumb2.com"]), + ], + ) m3.set("ssl_data", Media("ssl_data.txt", urls=["ssl_data.com"]).to_dict()) m3.set("bad_data", {"bad": "dict is ignored"}) From 9529784fa288924817775f6db7ce6feff536a1c5 Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Fri, 28 Feb 2025 11:46:39 -0600 Subject: [PATCH 10/40] Format and lint `migrations` directory (#59) --- .pre-commit-config.yaml | 6 +-- app/migrations/env.py | 7 ++- ...7ed0_create_archives_store_until_column.py | 18 ++++--- ...24ec4b1_rename_sheets_last_archived_col.py | 21 +++++--- ...dd_new_service_account_email_column_to_.py | 13 +++-- ...21d2c96d8_add_sheet_id_to_archive_table.py | 34 +++++++------ ...45b_modify_archive_url_to_have_uuid_id_.py | 8 +-- ...vacuum_database_if_there_s_enough_space.py | 9 ++-- .../a23aaf3ae930_drop_active_column.py | 25 +++++++--- ...a012ec405b8_add_columns_to_groups_table.py | 49 ++++++++++++------- 10 files changed, 120 insertions(+), 70 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e1d2a8..edb6bcf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/nbQA-dev/nbQA - rev: 1.8.5 + rev: 1.9.1 hooks: - id: nbqa-ruff args: @@ -17,7 +17,7 @@ repos: - --profile=black - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: check-docstring-first @@ -49,7 +49,7 @@ repos: - id: python-no-log-warn - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 6.0.1 hooks: - id: isort name: Run isort to sort imports diff --git a/app/migrations/env.py b/app/migrations/env.py index 1f579ab..54c1f48 100644 --- a/app/migrations/env.py +++ b/app/migrations/env.py @@ -9,11 +9,14 @@ from app.shared.settings import get_settings # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -config.set_main_option('sqlalchemy.url', get_settings().DATABASE_PATH) +config.set_main_option("sqlalchemy.url", get_settings().DATABASE_PATH) # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: - fileConfig(config.config_file_name, disable_existing_loggers=False) # disable_existing_loggers prevents loguru disabling + # disable_existing_loggers prevents loguru disabling + fileConfig( + config.config_file_name, disable_existing_loggers=False + ) # add your model's MetaData object here # for 'autogenerate' support diff --git a/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py b/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py index 8642f2b..1bb5695 100644 --- a/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py +++ b/app/migrations/versions/02b2f6d17ed0_create_archives_store_until_column.py @@ -5,13 +5,14 @@ Revises: 1636724ec4b1 Create Date: 2025-02-08 15:22:20.392522 """ + import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = '02b2f6d17ed0' -down_revision = '1636724ec4b1' +revision = "02b2f6d17ed0" +down_revision = "1636724ec4b1" branch_labels = None depends_on = None STORE_UNTIL_COL = "store_until" @@ -20,15 +21,20 @@ STORE_UNTIL_COL = "store_until" def upgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns('archives')] + columns = [col["name"] for col in inspector.get_columns("archives")] if STORE_UNTIL_COL not in columns: - op.add_column('archives', sa.Column(STORE_UNTIL_COL, sa.DateTime(), nullable=True, default=None)) + op.add_column( + "archives", + sa.Column( + STORE_UNTIL_COL, sa.DateTime(), nullable=True, default=None + ), + ) def downgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns('archives')] + columns = [col["name"] for col in inspector.get_columns("archives")] if STORE_UNTIL_COL in columns: - op.drop_column('archives', STORE_UNTIL_COL) + op.drop_column("archives", STORE_UNTIL_COL) diff --git a/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py b/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py index 324f75a..ad972fc 100644 --- a/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py +++ b/app/migrations/versions/1636724ec4b1_rename_sheets_last_archived_col.py @@ -5,13 +5,14 @@ Revises: a23aaf3ae930 Create Date: 2025-02-05 19:19:01.984396 """ + import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = '1636724ec4b1' -down_revision = 'a23aaf3ae930' +revision = "1636724ec4b1" +down_revision = "a23aaf3ae930" branch_labels = None depends_on = None @@ -19,14 +20,18 @@ depends_on = None def upgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns('sheets')] - if 'last_archived_at' in columns: - op.alter_column('sheets', 'last_archived_at', new_column_name='last_url_archived_at') + columns = [col["name"] for col in inspector.get_columns("sheets")] + if "last_archived_at" in columns: + op.alter_column( + "sheets", "last_archived_at", new_column_name="last_url_archived_at" + ) def downgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns('sheets')] - if 'last_url_archived_at' in columns: - op.alter_column('sheets', 'last_url_archived_at', new_column_name='last_archived_at') + columns = [col["name"] for col in inspector.get_columns("sheets")] + if "last_url_archived_at" in columns: + op.alter_column( + "sheets", "last_url_archived_at", new_column_name="last_archived_at" + ) diff --git a/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py b/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py index 572905d..0ea0b11 100644 --- a/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py +++ b/app/migrations/versions/63ac79df4ad0_add_new_service_account_email_column_to_.py @@ -5,13 +5,14 @@ Revises: 02b2f6d17ed0 Create Date: 2025-02-11 21:53:23.293274 """ + import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = '63ac79df4ad0' -down_revision = '02b2f6d17ed0' +revision = "63ac79df4ad0" +down_revision = "02b2f6d17ed0" branch_labels = None depends_on = None @@ -22,15 +23,17 @@ TABLE = "groups" def upgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns(TABLE)] + columns = [col["name"] for col in inspector.get_columns(TABLE)] if NEW_COL not in columns: - op.add_column(TABLE, sa.Column(NEW_COL, sa.String, nullable=True, default=None)) + op.add_column( + TABLE, sa.Column(NEW_COL, sa.String, nullable=True, default=None) + ) def downgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns(TABLE)] + columns = [col["name"] for col in inspector.get_columns(TABLE)] if NEW_COL in columns: op.drop_column(TABLE, NEW_COL) diff --git a/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py b/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py index 892e853..e34e06d 100644 --- a/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py +++ b/app/migrations/versions/89121d2c96d8_add_sheet_id_to_archive_table.py @@ -5,14 +5,14 @@ Revises: fa012ec405b8 Create Date: 2024-11-04 11:12:30.237299 """ + import sqlalchemy as sa from alembic import op -from sqlalchemy.engine.reflection import Inspector # revision identifiers, used by Alembic. -revision = '89121d2c96d8' -down_revision = 'fa012ec405b8' +revision = "89121d2c96d8" +down_revision = "fa012ec405b8" branch_labels = None depends_on = None @@ -20,23 +20,27 @@ depends_on = None def upgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns('archives')] + columns = [col["name"] for col in inspector.get_columns("archives")] - if 'sheet_id' not in columns: - with op.batch_alter_table('archives') as batch_op: - batch_op.add_column(sa.Column('sheet_id', sa.String(), nullable=True, default=None)) - batch_op.create_foreign_key('fk_sheet_id', 'sheets', ['sheet_id'], ['id']) + if "sheet_id" not in columns: + with op.batch_alter_table("archives") as batch_op: + batch_op.add_column( + sa.Column("sheet_id", sa.String(), nullable=True, default=None) + ) + batch_op.create_foreign_key( + "fk_sheet_id", "sheets", ["sheet_id"], ["id"] + ) def downgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - foreign_keys = [fk['name'] for fk in inspector.get_foreign_keys('archives')] - columns = [col['name'] for col in inspector.get_columns('archives')] + foreign_keys = [fk["name"] for fk in inspector.get_foreign_keys("archives")] + columns = [col["name"] for col in inspector.get_columns("archives")] - with op.batch_alter_table('archives') as batch_op: - if 'fk_sheet_id' in foreign_keys: - batch_op.drop_constraint('fk_sheet_id', type_='foreignkey') + with op.batch_alter_table("archives") as batch_op: + if "fk_sheet_id" in foreign_keys: + batch_op.drop_constraint("fk_sheet_id", type_="foreignkey") - if 'sheet_id' in columns: - batch_op.drop_column('sheet_id') + if "sheet_id" in columns: + batch_op.drop_column("sheet_id") diff --git a/app/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py b/app/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py index bdfe474..1f7f348 100644 --- a/app/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py +++ b/app/migrations/versions/9369a264945b_modify_archive_url_to_have_uuid_id_.py @@ -5,11 +5,12 @@ Revises: Create Date: 2023-12-20 17:24:59.320691 """ + from alembic import op # revision identifiers, used by Alembic. -revision = '9369a264945b' +revision = "9369a264945b" down_revision = None branch_labels = None depends_on = None @@ -19,10 +20,11 @@ def upgrade() -> None: # since the primary key constraint is not named, we have to recreate it first with op.batch_alter_table("archive_urls") as batch_op: batch_op.create_primary_key("pk_url", ["url"]) - batch_op.drop_constraint("pk_url", type_='primary') + batch_op.drop_constraint("pk_url", type_="primary") batch_op.create_primary_key("pk_url_archive_id", ["url", "archive_id"]) + def downgrade() -> None: with op.batch_alter_table("archive_urls") as batch_op: - batch_op.drop_constraint("pk_url_archive_id", type_='primary') + batch_op.drop_constraint("pk_url_archive_id", type_="primary") batch_op.create_primary_key("url", ["url"]) diff --git a/app/migrations/versions/93a611e4c066_vacuum_database_if_there_s_enough_space.py b/app/migrations/versions/93a611e4c066_vacuum_database_if_there_s_enough_space.py index 6b8d8d2..b099f59 100644 --- a/app/migrations/versions/93a611e4c066_vacuum_database_if_there_s_enough_space.py +++ b/app/migrations/versions/93a611e4c066_vacuum_database_if_there_s_enough_space.py @@ -5,12 +5,13 @@ Revises: 9369a264945b Create Date: 2023-12-20 18:33:27.132566 """ + from alembic import op # revision identifiers, used by Alembic. -revision = '93a611e4c066' -down_revision = '9369a264945b' +revision = "93a611e4c066" +down_revision = "9369a264945b" branch_labels = None depends_on = None @@ -20,7 +21,9 @@ def upgrade() -> None: with op.get_context().autocommit_block(): op.execute("VACUUM") except Exception as e: - print("Unable to run vacuum, maybe there's not enough disk space. it should be 2x the size of the database") + print( + "Unable to run vacuum, maybe there's not enough disk space. it should be 2x the size of the database" + ) print(e) diff --git a/app/migrations/versions/a23aaf3ae930_drop_active_column.py b/app/migrations/versions/a23aaf3ae930_drop_active_column.py index ebc85e7..aa8f97b 100644 --- a/app/migrations/versions/a23aaf3ae930_drop_active_column.py +++ b/app/migrations/versions/a23aaf3ae930_drop_active_column.py @@ -5,13 +5,14 @@ Revises: 89121d2c96d8 Create Date: 2025-02-04 12:19:20.753570 """ + import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = 'a23aaf3ae930' -down_revision = '89121d2c96d8' +revision = "a23aaf3ae930" +down_revision = "89121d2c96d8" branch_labels = None depends_on = None @@ -19,16 +20,24 @@ depends_on = None def upgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns('users')] + columns = [col["name"] for col in inspector.get_columns("users")] - if 'is_active' in columns: - op.drop_column('users', 'is_active') + if "is_active" in columns: + op.drop_column("users", "is_active") def downgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns('users')] + columns = [col["name"] for col in inspector.get_columns("users")] - if 'is_active' not in columns: - op.add_column('users', sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.false())) + if "is_active" not in columns: + op.add_column( + "users", + sa.Column( + "is_active", + sa.Boolean(), + nullable=False, + server_default=sa.false(), + ), + ) diff --git a/app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py b/app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py index c3169c3..f5cbcaa 100644 --- a/app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py +++ b/app/migrations/versions/fa012ec405b8_add_columns_to_groups_table.py @@ -5,14 +5,14 @@ Revises: 93a611e4c066 Create Date: 2024-10-31 09:36:50.360710 """ + import sqlalchemy as sa from alembic import op -from sqlalchemy.engine.reflection import Inspector # revision identifiers, used by Alembic. -revision = 'fa012ec405b8' -down_revision = '93a611e4c066' +revision = "fa012ec405b8" +down_revision = "93a611e4c066" branch_labels = None depends_on = None @@ -20,26 +20,41 @@ depends_on = None def upgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns('groups')] + columns = [col["name"] for col in inspector.get_columns("groups")] - if 'description' not in columns: - op.add_column('groups', sa.Column('description', sa.String(), nullable=True)) - if 'orchestrator' not in columns: - op.add_column('groups', sa.Column('orchestrator', sa.String(), nullable=True)) - if 'orchestrator_sheet' not in columns: - op.add_column('groups', sa.Column('orchestrator_sheet', sa.String(), nullable=True)) - if 'permissions' not in columns: - op.add_column('groups', sa.Column('permissions', sa.JSON(), nullable=True)) - if 'domains' not in columns: - op.add_column('groups', sa.Column('domains', sa.JSON(), nullable=True)) + if "description" not in columns: + op.add_column( + "groups", sa.Column("description", sa.String(), nullable=True) + ) + if "orchestrator" not in columns: + op.add_column( + "groups", sa.Column("orchestrator", sa.String(), nullable=True) + ) + if "orchestrator_sheet" not in columns: + op.add_column( + "groups", + sa.Column("orchestrator_sheet", sa.String(), nullable=True), + ) + if "permissions" not in columns: + op.add_column( + "groups", sa.Column("permissions", sa.JSON(), nullable=True) + ) + if "domains" not in columns: + op.add_column("groups", sa.Column("domains", sa.JSON(), nullable=True)) def downgrade() -> None: conn = op.get_bind() inspector = sa.inspect(conn) - columns = [col['name'] for col in inspector.get_columns('groups')] + columns = [col["name"] for col in inspector.get_columns("groups")] - column_names = ['description', 'orchestrator', 'orchestrator_sheet', 'permissions', 'domains'] + column_names = [ + "description", + "orchestrator", + "orchestrator_sheet", + "permissions", + "domains", + ] for column_name in column_names: if column_name in columns: - op.drop_column('groups', column_name) + op.drop_column("groups", column_name) From 85cec9fbb94ed31b5840108666dc86a69310e388 Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Mon, 3 Mar 2025 08:10:23 -0600 Subject: [PATCH 11/40] Consolidate and organize config files (#61) --- .coveragerc | 3 --- docker-compose.yml | 4 ++-- web.Dockerfile => docker/web/Dockerfile | 8 ++++---- worker.Dockerfile => docker/worker/Dockerfile | 8 ++++---- pyproject.toml | 7 +++++++ pytest.ini | 2 -- 6 files changed, 17 insertions(+), 15 deletions(-) delete mode 100644 .coveragerc rename web.Dockerfile => docker/web/Dockerfile (76%) rename worker.Dockerfile => docker/worker/Dockerfile (86%) delete mode 100644 pytest.ini diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index f6cc4e8..0000000 --- a/.coveragerc +++ /dev/null @@ -1,3 +0,0 @@ -[run] -omit = - app/migrations/* diff --git a/docker-compose.yml b/docker-compose.yml index 737c346..635f7cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: web: build: context: . - dockerfile: web.Dockerfile + dockerfile: docker/web/Dockerfile restart: always env_file: .env.prod environment: @@ -31,7 +31,7 @@ services: worker: build: context: . - dockerfile: worker.Dockerfile + dockerfile: docker/worker/Dockerfile restart: always env_file: .env.prod command: celery --app=app.worker.main.celery worker -Q high_priority,low_priority --concurrency=${CONCURRENCY} --max-tasks-per-child=100 diff --git a/web.Dockerfile b/docker/web/Dockerfile similarity index 76% rename from web.Dockerfile rename to docker/web/Dockerfile index d142877..cfd60d6 100644 --- a/web.Dockerfile +++ b/docker/web/Dockerfile @@ -10,13 +10,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir poetry -COPY pyproject.toml poetry.lock README.md . +COPY ../../pyproject.toml ../../poetry.lock ../../README.md ./ RUN poetry install --with web --no-interaction --no-ansi --no-cache # Copy the application code and configurations -COPY alembic.ini ./ -COPY ./app/ ./app/ -COPY user-groups.* ./app/ +COPY ../../alembic.ini ./ +COPY ../../app ./app/ +COPY ../../user-groups.* ./app/ # Run the FastAPI app with Uvicorn ENTRYPOINT ["poetry", "run"] diff --git a/worker.Dockerfile b/docker/worker/Dockerfile similarity index 86% rename from worker.Dockerfile rename to docker/worker/Dockerfile index 9154c32..23c1e45 100644 --- a/worker.Dockerfile +++ b/docker/worker/Dockerfile @@ -20,14 +20,14 @@ RUN apt update -y && \ python3 -m venv ./poetry-venv && \ ./poetry-venv/bin/python -m pip install --upgrade pip && \ ./poetry-venv/bin/python -m pip install "poetry>=2.0.0,<3.0.0" -COPY pyproject.toml poetry.lock ./ +COPY ../../pyproject.toml ../../poetry.lock ./ RUN ./poetry-venv/bin/poetry install --without dev --no-root --no-cache # install dependencies # copy source code and .env files over -COPY alembic.ini ./ -COPY ./app/ ./app/ -COPY user-groups.* ./app/ +COPY ../../alembic.ini ./ +COPY ../../app ./app/ +COPY ../../user-groups.* ./app/ ENTRYPOINT ["./poetry-venv/bin/poetry", "run"] diff --git a/pyproject.toml b/pyproject.toml index df7b789..75f6e63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,13 @@ dependencies = [ "requests (>=2.25.1)", "pyopenssl (>=23.3.0)", ] + +[tool.pytest.ini_options] +pythonpath = "." + +[tool.coverage.run] +omit = ["app/migrations/*"] + [tool.poetry.group.worker.dependencies] watchdog = ">=6.0.0,<7.0.0" setuptools = "^75.8.0" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index a635c5c..0000000 --- a/pytest.ini +++ /dev/null @@ -1,2 +0,0 @@ -[pytest] -pythonpath = . From b5571fc61fae667a7973125ae04121876e83764b Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Mon, 3 Mar 2025 11:59:04 -0600 Subject: [PATCH 12/40] Format and lint `workers` directory (#60) --- app/worker/main.py | 81 ++++++++++++++++++++++++++++++---------- app/worker/worker_log.py | 45 ++++++++++++---------- 2 files changed, 87 insertions(+), 39 deletions(-) diff --git a/app/worker/main.py b/app/worker/main.py index 7b4826b..f7b2915 100644 --- a/app/worker/main.py +++ b/app/worker/main.py @@ -30,8 +30,16 @@ setup_celery_logger(celery) # logger.add("app/worker/worker_log.log", level="DEBUG") logger.remove = lambda x: print(f"logger.remove({x})") -# TODO: after release, as it requires updating past entries with sheet_id where tag is used, drop tags -@celery.task(name="create_archive_task", bind=True, autoretry_for=(Exception,), retry_backoff=True, retry_kwargs={'max_retries': 1}) + +# TODO: after release, as it requires updating past entries with sheet_id where tag +# is used, drop tags +@celery.task( + name="create_archive_task", + bind=True, + autoretry_for=(Exception,), + retry_backoff=True, + retry_kwargs={"max_retries": 1}, +) def create_archive_task(self, archive_json: str): archive = schemas.ArchiveCreate.model_validate_json(archive_json) @@ -42,9 +50,9 @@ def create_archive_task(self, archive_json: str): orchestrator.setup(args) result = next(orchestrator.feed()) except SystemExit as e: - log_error(e, f"create_archive_task: SystemExit from AA") + log_error(e, "create_archive_task: SystemExit from AA") except Exception as e: - log_error(e, f"create_archive_task") + log_error(e, "create_archive_task") raise e assert result, f"UNABLE TO archive: {archive.url}" @@ -61,10 +69,14 @@ def create_archive_task(self, archive_json: str): @celery.task(name="create_sheet_task", bind=True) def create_sheet_task(self, sheet_json: str): sheet = schemas.SubmitSheet.model_validate_json(sheet_json) - queue_name = (create_sheet_task.request.delivery_info or {}).get('routing_key', 'unknown') + queue_name = (create_sheet_task.request.delivery_info or {}).get( + "routing_key", "unknown" + ) logger.info(f"[queue={queue_name}] SHEET START {sheet=}") - args = get_orchestrator_args(sheet.group_id, True, ["--gsheet_feeder.sheet_id", sheet.sheet_id]) + args = get_orchestrator_args( + sheet.group_id, True, ["--gsheet_feeder.sheet_id", sheet.sheet_id] + ) orchestrator = ArchivingOrchestrator() orchestrator.setup(args) @@ -82,7 +94,7 @@ def create_sheet_task(self, sheet_json: str): result=json.loads(result.to_json()), sheet_id=sheet.sheet_id, urls=get_all_urls(result), - store_until=get_store_until(sheet.group_id) + store_until=get_store_until(sheet.group_id), ) insert_result_into_db(archive) stats["archived"] += 1 @@ -95,25 +107,39 @@ def create_sheet_task(self, sheet_json: str): stats["errors"].append(str(e)) except SystemExit as e: - log_error(e, f"create_sheet_task: SystemExit from AA") + log_error(e, "create_sheet_task: SystemExit from AA") if stats["archived"] > 0: with get_db() as session: - worker_crud.update_sheet_last_url_archived_at(session, sheet.sheet_id) + worker_crud.update_sheet_last_url_archived_at( + session, sheet.sheet_id + ) logger.info(f"SHEET DONE {sheet=}") # TODO: is this used anywhere? maybe drop it - return schemas.CelerySheetTask(success=True, sheet_id=sheet.sheet_id, time=datetime.datetime.now().isoformat(), stats=stats).model_dump() + return schemas.CelerySheetTask( + success=True, + sheet_id=sheet.sheet_id, + time=datetime.datetime.now().isoformat(), + stats=stats, + ).model_dump() -def get_orchestrator_args(group_id: str, orchestrator_for_sheet: bool, cli_args: list = []) -> list: +def get_orchestrator_args( + group_id: str, orchestrator_for_sheet: bool, cli_args: list = None +) -> list: + if cli_args is None: + cli_args = [] + aa_configs = [] with get_db() as session: group = worker_crud.get_group(session, group_id) if orchestrator_for_sheet: orchestrator_fn = group.orchestrator_sheet else: - orchestrator_fn = worker_crud.get_group(session, group_id).orchestrator + orchestrator_fn = worker_crud.get_group( + session, group_id + ).orchestrator assert orchestrator_fn, f"no orchestrator found for {group_id}" aa_configs.extend(["--config", orchestrator_fn]) aa_configs.extend(cli_args) @@ -123,7 +149,9 @@ def get_orchestrator_args(group_id: str, orchestrator_for_sheet: bool, cli_args: def insert_result_into_db(archive: schemas.ArchiveCreate) -> str: with get_db() as session: db_archive = worker_crud.store_archived_url(session, archive) - logger.debug(f"[ARCHIVE STORED] {db_archive.author_id} {db_archive.url}") + logger.debug( + f"[ARCHIVE STORED] {db_archive.author_id} {db_archive.url}" + ) return db_archive.id @@ -132,13 +160,22 @@ def get_store_until(group_id: str) -> datetime.datetime: return business_logic.get_store_archive_until(session, group_id) -def redis_publish_exception(exception, task_name, traceback: str = ""): +def redis_publish_exception(exception, task_name, trace_back: str = ""): REDIS_EXCEPTIONS_CHANNEL = settings.REDIS_EXCEPTIONS_CHANNEL try: - exception_data = {"task": task_name, "type": exception.__class__.__name__, "exception": exception, "traceback": traceback} - Redis.publish(REDIS_EXCEPTIONS_CHANNEL, json.dumps(exception_data, default=str)) + exception_data = { + "task": task_name, + "type": exception.__class__.__name__, + "exception": exception, + "traceback": trace_back, + } + Redis.publish( + REDIS_EXCEPTIONS_CHANNEL, json.dumps(exception_data, default=str) + ) except Exception as e: - log_error(e, f"[CRITICAL] Could not publish to {REDIS_EXCEPTIONS_CHANNEL}") + log_error( + e, f"[CRITICAL] Could not publish to {REDIS_EXCEPTIONS_CHANNEL}" + ) @task_failure.connect(sender=create_sheet_task) @@ -146,6 +183,10 @@ def redis_publish_exception(exception, task_name, traceback: str = ""): def task_failure_notifier(sender, **kwargs): # automatically capture exceptions in the worker tasks logger.warning(f"⚠️ worker task failed: {sender.name}") - traceback_msg = "\n".join(traceback.format_list(traceback.extract_tb(kwargs['traceback']))) - log_error(kwargs['exception'], traceback_msg, f"task_failure: {sender.name}") - redis_publish_exception(kwargs['exception'], sender.name, traceback_msg) + traceback_msg = "\n".join( + traceback.format_list(traceback.extract_tb(kwargs["traceback"])) + ) + log_error( + kwargs["exception"], traceback_msg, f"task_failure: {sender.name}" + ) + redis_publish_exception(kwargs["exception"], sender.name, traceback_msg) diff --git a/app/worker/worker_log.py b/app/worker/worker_log.py index 022c63d..c1c8dcc 100644 --- a/app/worker/worker_log.py +++ b/app/worker/worker_log.py @@ -1,6 +1,5 @@ import sys -from celery import Celery from loguru import logger from app.shared.task_messaging import get_celery @@ -8,24 +7,32 @@ from app.shared.task_messaging import get_celery celery = get_celery("worker") -def setup_celery_logger(celery): - # Remove Celery's default handlers to prevent duplicate logs - celery_logger = celery.log.get_default_logger() - for handler in celery_logger.handlers[:]: - celery_logger.removeHandler(handler) - # Set up Loguru logging - logger.add("logs/celery_logs.log", retention="30 days", level="DEBUG") - logger.add("logs/celery_error_logs.log", retention="30 days", level="ERROR") +def setup_celery_logger(c): + # Remove Celery's default handlers to prevent duplicate logs + celery_logger = c.log.get_default_logger() + for handler in celery_logger.handlers[:]: + celery_logger.removeHandler(handler) - # Redirect Celery logs to Loguru - class InterceptHandler: - def write(self, message): - if message.strip(): - logger.info(message.strip()) - # Required to prevent issues with buffered output - def flush(self): pass - def isatty(self): return False + # Set up Loguru logging + logger.add("logs/celery_logs.log", retention="30 days", level="DEBUG") + logger.add("logs/celery_error_logs.log", retention="30 days", level="ERROR") - sys.stdout = InterceptHandler() - sys.stderr = InterceptHandler() + # Redirect Celery logs to Loguru + class InterceptHandler: + @staticmethod + def write(message): + if message.strip(): + logger.info(message.strip()) + + # Required to prevent issues with buffered output + @staticmethod + def flush(): + pass + + @staticmethod + def isatty(): + return False + + sys.stdout = InterceptHandler() + sys.stderr = InterceptHandler() From a9ca410d08f326737199fdf73e3e357880c180f7 Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Mon, 3 Mar 2025 13:20:11 -0600 Subject: [PATCH 13/40] Move `alembic.ini` to `migrations` directory (#66) --- alembic.ini => app/migrations/alembic.ini | 6 +- app/tests/web/test_main.py | 20 +++- app/web/events.py | 114 +++++++++++++++++----- docker/web/Dockerfile | 1 - docker/worker/Dockerfile | 1 - 5 files changed, 110 insertions(+), 32 deletions(-) rename alembic.ini => app/migrations/alembic.ini (95%) diff --git a/alembic.ini b/app/migrations/alembic.ini similarity index 95% rename from alembic.ini rename to app/migrations/alembic.ini index 30d7030..fa1578b 100644 --- a/alembic.ini +++ b/app/migrations/alembic.ini @@ -2,7 +2,7 @@ [alembic] # path to migration scripts -script_location = app/migrations +script_location = ./app/migrations # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s # Uncomment the line below if you want the files to be prepended with date and time @@ -10,10 +10,6 @@ script_location = app/migrations # for all available tokens # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s -# sys.path path, will be prepended to sys.path if present. -# defaults to the current working directory. -prepend_sys_path = . - # timezone to use when rendering the date within the migration file # as well as the filename. # If specified, requires the python-dateutil library that can be diff --git a/app/tests/web/test_main.py b/app/tests/web/test_main.py index 0dbd5fa..eb985a8 100644 --- a/app/tests/web/test_main.py +++ b/app/tests/web/test_main.py @@ -18,8 +18,24 @@ def test_lifespan(app): def test_alembic(db_session): - alembic.config.main(argv=["--raiseerr", "upgrade", "head"]) - alembic.config.main(argv=["--raiseerr", "downgrade", "base"]) + alembic.config.main( + argv=[ + "-c", + "./app/migrations/alembic.ini", + "--raiseerr", + "upgrade", + "head", + ] + ) + alembic.config.main( + argv=[ + "-c", + "./app/migrations/alembic.ini", + "--raiseerr", + "downgrade", + "base", + ] + ) @patch( diff --git a/app/web/events.py b/app/web/events.py index fa15614..17a24b9 100644 --- a/app/web/events.py +++ b/app/web/events.py @@ -38,9 +38,22 @@ async def lifespan(app: FastAPI): # STARTUP engine = make_engine(get_settings().DATABASE_PATH) models.Base.metadata.create_all(bind=engine) - alembic.config.main(prog="alembic", argv=['--raiseerr', 'upgrade', 'head']) + alembic.config.main( + prog="alembic", + argv=[ + "-c", + "./app/migrations/alembic.ini", + "--raiseerr", + "upgrade", + "head", + ], + ) logging.getLogger("uvicorn.access").disabled = True # loguru - asyncio.create_task(redis_subscribe_worker_exceptions(get_settings().REDIS_EXCEPTIONS_CHANNEL)) + asyncio.create_task( + redis_subscribe_worker_exceptions( + get_settings().REDIS_EXCEPTIONS_CHANNEL + ) + ) asyncio.create_task(repeat_measure_regular_metrics()) with get_db() as db: crud.upsert_user_groups(db) @@ -71,41 +84,74 @@ async def lifespan(app: FastAPI): # CRON JOBS -@repeat_every(seconds=get_settings().REPEAT_COUNT_METRICS_SECONDS, on_exception=increase_exceptions_counter) +@repeat_every( + seconds=get_settings().REPEAT_COUNT_METRICS_SECONDS, + on_exception=increase_exceptions_counter, +) async def repeat_measure_regular_metrics(): - await measure_regular_metrics(get_settings().DATABASE_PATH, get_settings().REPEAT_COUNT_METRICS_SECONDS) + await measure_regular_metrics( + get_settings().DATABASE_PATH, + get_settings().REPEAT_COUNT_METRICS_SECONDS, + ) -@repeat_every(seconds=60, wait_first=120, on_exception=increase_exceptions_counter) +@repeat_every( + seconds=60, wait_first=120, on_exception=increase_exceptions_counter +) async def archive_hourly_sheets_cronjob(): await archive_sheets_cronjob("hourly", 60, datetime.datetime.now().minute) -@repeat_every(seconds=3600, wait_first=120, on_exception=increase_exceptions_counter) +@repeat_every( + seconds=3600, wait_first=120, on_exception=increase_exceptions_counter +) async def archive_daily_sheets_cronjob(): await archive_sheets_cronjob("daily", 24, datetime.datetime.now().hour) -async def archive_sheets_cronjob(frequency: str, interval: int, current_time_unit: int): +async def archive_sheets_cronjob( + frequency: str, interval: int, current_time_unit: int +): triggered_jobs = [] async with get_db_async() as db: - sheets = await crud.get_sheets_by_id_hash(db, frequency, interval, current_time_unit) + sheets = await crud.get_sheets_by_id_hash( + db, frequency, str(interval), current_time_unit + ) for s in sheets: group_queue = await crud.get_group_priority_async(db, s.group_id) - task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=s.id, author_id=s.author_id, group_id=s.group_id).model_dump_json()]).apply_async(**group_queue) + task = celery.signature( + "create_sheet_task", + args=[ + schemas.SubmitSheet( + sheet_id=s.id, + author_id=s.author_id, + group_id=s.group_id, + ).model_dump_json() + ], + ).apply_async(**group_queue) triggered_jobs.append({"sheet_id": s.id, "task_id": task.id}) - logger.debug(f"[CRON {frequency.upper()}:{current_time_unit}] Triggered {len(triggered_jobs)} sheet tasks: {triggered_jobs}") + logger.debug( + f"[CRON {frequency.upper()}:{current_time_unit}] Triggered {len(triggered_jobs)} sheet tasks: {triggered_jobs}" + ) # TODO: on exception should logerror but also prometheus counter -DELETE_WINDOW = get_settings().DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS * 24 * 60 * 60 +DELETE_WINDOW = ( + get_settings().DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS * 24 * 60 * 60 +) -@repeat_every(seconds=DELETE_WINDOW, wait_first=180, on_exception=increase_exceptions_counter) +@repeat_every( + seconds=DELETE_WINDOW, + wait_first=180, + on_exception=increase_exceptions_counter, +) async def notify_about_expired_archives(): - notify_from = datetime.datetime.now() + datetime.timedelta(days=get_settings().DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS) + notify_from = datetime.datetime.now() + datetime.timedelta( + days=get_settings().DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS + ) async with get_db_async() as db: scheduled_deletions = await crud.find_by_store_until(db, notify_from) @@ -117,7 +163,12 @@ async def notify_about_expired_archives(): fastmail = FastMail(get_settings().MAIL_CONFIG) # notify users for email in user_archives: - list_of_archives = "\n".join([f'{a.url}, {a.id}, {a.store_until.isoformat()}
' for a in user_archives[email]]) + list_of_archives = "\n".join( + [ + f"{a.url}, {a.id}, {a.store_until.isoformat()}
" + for a in user_archives[email] + ] + ) # TODO: how can users download them in bulk? message = MessageSchema( subject="Auto Archiver: Archives Scheduled for Deletion", @@ -137,16 +188,23 @@ async def notify_about_expired_archives(): """, - subtype=MessageType.html + subtype=MessageType.html, ) await fastmail.send_message(message) - logger.debug(f"[CRON] Email sent to {email} about {len(user_archives[email])} scheduled archives deletion.") + logger.debug( + f"[CRON] Email sent to {email} about {len(user_archives[email])} scheduled archives deletion." + ) # now schedule the deletion event asyncio.create_task(delete_expired_archives()) -@repeat_every(max_repetitions=1, wait_first=10, seconds=0, on_exception=increase_exceptions_counter) +@repeat_every( + max_repetitions=1, + wait_first=10, + seconds=0, + on_exception=increase_exceptions_counter, +) async def delete_expired_archives(): async with get_db_async() as db: count_deleted = await crud.soft_delete_expired_archives(db) @@ -154,19 +212,27 @@ async def delete_expired_archives(): logger.debug(f"[CRON] Deleted {count_deleted} archives.") -@repeat_every(seconds=86400, wait_first=150, on_exception=increase_exceptions_counter) +@repeat_every( + seconds=86400, wait_first=150, on_exception=increase_exceptions_counter +) async def delete_stale_sheets(): STALE_DAYS = get_settings().DELETE_STALE_SHEETS_DAYS logger.debug(f"[CRON] Deleting stale sheets older than {STALE_DAYS} days.") async with get_db_async() as db: user_sheets = await crud.delete_stale_sheets(db, STALE_DAYS) - if not user_sheets: return + if not user_sheets: + return fastmail = FastMail(get_settings().MAIL_CONFIG) # notify users for email in user_sheets: - list_of_sheets = "\n".join([f'
  • {s.name}
  • ' for s in user_sheets[email]]) + list_of_sheets = "\n".join( + [ + f'
  • {s.name}
  • ' + for s in user_sheets[email] + ] + ) message = MessageSchema( subject="Auto Archiver: Stale Sheets Removed", recipients=[email], @@ -183,14 +249,16 @@ async def delete_stale_sheets(): """, - subtype=MessageType.html + subtype=MessageType.html, ) await fastmail.send_message(message) - logger.debug(f"[CRON] Email sent to {email} about stale sheets deletion.") + logger.debug( + f"[CRON] Email sent to {email} about stale sheets deletion." + ) # @repeat_at async def generate_users_export_csv(): - #TODO: implement a cronjob that regularly requested user data to a CSV file + # TODO: implement a cronjob that regularly requested user data to a CSV file # see https://colab.research.google.com/drive/1QDbo3QXHPBdiTuANlA1AWVvN-rqxuCPa?authuser=0#scrollTo=4nPXeSdK8RBT pass diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile index cfd60d6..03e6a34 100644 --- a/docker/web/Dockerfile +++ b/docker/web/Dockerfile @@ -14,7 +14,6 @@ COPY ../../pyproject.toml ../../poetry.lock ../../README.md ./ RUN poetry install --with web --no-interaction --no-ansi --no-cache # Copy the application code and configurations -COPY ../../alembic.ini ./ COPY ../../app ./app/ COPY ../../user-groups.* ./app/ diff --git a/docker/worker/Dockerfile b/docker/worker/Dockerfile index 23c1e45..288aad5 100644 --- a/docker/worker/Dockerfile +++ b/docker/worker/Dockerfile @@ -26,7 +26,6 @@ RUN ./poetry-venv/bin/poetry install --without dev --no-root --no-cache # install dependencies # copy source code and .env files over -COPY ../../alembic.ini ./ COPY ../../app ./app/ COPY ../../user-groups.* ./app/ From 1ca0ae2fb2d7ca21f9968f84da3e22d4d7fa4d45 Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Mon, 3 Mar 2025 13:20:50 -0600 Subject: [PATCH 14/40] Format and lint `shared` directory (#64) --- app/shared/aa_utils.py | 28 +++++++--- app/shared/business_logic.py | 25 ++++++--- app/shared/db/database.py | 34 ++++++++---- app/shared/db/models.py | 48 +++++++++++++---- app/shared/db/worker_crud.py | 39 +++++++++++--- app/shared/log.py | 8 +-- app/shared/schemas.py | 2 + app/shared/settings.py | 30 +++++++---- app/shared/task_messaging.py | 11 ++-- app/shared/user_groups.py | 102 +++++++++++++++++++++++++---------- app/shared/utils/misc.py | 22 ++++---- app/tests/conftest.py | 4 +- app/web/events.py | 4 +- 13 files changed, 255 insertions(+), 102 deletions(-) diff --git a/app/shared/aa_utils.py b/app/shared/aa_utils.py index 466d003..1021f8e 100644 --- a/app/shared/aa_utils.py +++ b/app/shared/aa_utils.py @@ -11,23 +11,39 @@ from app.shared.db import models def get_all_urls(result: Metadata) -> List[models.ArchiveUrl]: db_urls = [] for m in result.media: - for i, url in enumerate(m.urls): db_urls.append(models.ArchiveUrl(url=url, key=m.get("id", f"media_{i}"))) + for i, url in enumerate(m.urls): + db_urls.append( + models.ArchiveUrl(url=url, key=m.get("id", f"media_{i}")) + ) for k, prop in m.properties.items(): if prop_converted := convert_if_media(prop): - for i, url in enumerate(prop_converted.urls): db_urls.append(models.ArchiveUrl(url=url, key=prop_converted.get("id", f"{k}_{i}"))) + for i, url in enumerate(prop_converted.urls): + db_urls.append( + models.ArchiveUrl( + url=url, key=prop_converted.get("id", f"{k}_{i}") + ) + ) if isinstance(prop, list): for i, prop_media in enumerate(prop): if prop_media := convert_if_media(prop_media): for j, url in enumerate(prop_media.urls): - db_urls.append(models.ArchiveUrl(url=url, key=prop_media.get("id", f"{k}{prop_media.key}_{i}.{j}"))) + db_urls.append( + models.ArchiveUrl( + url=url, + key=prop_media.get( + "id", f"{k}{prop_media.key}_{i}.{j}" + ), + ) + ) return db_urls - def convert_if_media(media): - if isinstance(media, Media): return media + if isinstance(media, Media): + return media elif isinstance(media, dict): - try: return Media.from_dict(media) + try: + return Media.from_dict(media) except Exception as e: logger.debug(f"error parsing {media} : {e}") return False diff --git a/app/shared/business_logic.py b/app/shared/business_logic.py index b4291b6..fb8f2d7 100644 --- a/app/shared/business_logic.py +++ b/app/shared/business_logic.py @@ -1,26 +1,35 @@ -# TODO: temporary file for this code, maybe other code belongs here, maybe not. do decide - - import datetime +from typing import Union from sqlalchemy.orm import Session from app.shared.db import worker_crud -def get_store_archive_until(db: Session, group_id: str) -> datetime.datetime: +# TODO: temporary file for this code, maybe other code belongs here, maybe not. do +# decide + + +def get_store_archive_until( + db: Session, group_id: str +) -> Union[datetime.datetime, None]: group = worker_crud.get_group(db, group_id) assert group, f"Group {group_id} not found." - assert group.permissions and type(group.permissions) == dict, f"Group {group_id} has no permissions." + assert group.permissions and isinstance(group.permissions, dict), ( + f"Group {group_id} has no permissions." + ) max_lifespan = group.permissions.get("max_archive_lifespan_months", -1) - if max_lifespan == -1: return None + if max_lifespan == -1: + return None return datetime.datetime.now() + datetime.timedelta(days=30 * max_lifespan) -def get_store_archive_until_or_never(db: Session, group_id: str) -> datetime.datetime: +def get_store_archive_until_or_never( + db: Session, group_id: str +) -> Union[datetime.datetime, None]: try: return get_store_archive_until(db, group_id) - except AssertionError as e: + except AssertionError: return None diff --git a/app/shared/db/database.py b/app/shared/db/database.py index 51c235a..08297a1 100644 --- a/app/shared/db/database.py +++ b/app/shared/db/database.py @@ -18,9 +18,9 @@ def make_engine(database_url: str): engine = create_engine( database_url, connect_args={"check_same_thread": False}, - pool_size=15, # Increase pool size - max_overflow=20, # Allow more temporary connections - pool_recycle=1800 # Recycle connections every 30 minutes + pool_size=15, # Increase pool size + max_overflow=20, # Allow more temporary connections + pool_recycle=1800, # Recycle connections every 30 minutes ) @event.listens_for(engine, "connect") @@ -40,8 +40,10 @@ def make_session_local(engine: Engine): @contextmanager def get_db(): session = make_session_local(make_engine(get_settings().DATABASE_PATH))() - try: yield session - finally: session.close() + try: + yield session + finally: + session.close() def get_db_dependency(): @@ -59,22 +61,32 @@ def wal_checkpoint(): # ASYNC connections async def make_async_engine(database_url: str) -> AsyncEngine: - engine = create_async_engine(database_url, connect_args={"check_same_thread": False}) + engine = create_async_engine( + database_url, connect_args={"check_same_thread": False} + ) async with engine.begin() as conn: - await conn.run_sync(lambda sync_conn: sync_conn.execute(text("PRAGMA journal_mode=WAL;"))) + await conn.run_sync( + lambda sync_conn: sync_conn.execute( + text("PRAGMA journal_mode=WAL;") + ) + ) return engine async def make_async_session_local(engine: AsyncEngine) -> AsyncSession: - return async_sessionmaker(engine, expire_on_commit=False, autoflush=False, autocommit=False) + return async_sessionmaker( + engine, expire_on_commit=False, autoflush=False, autocommit=False + ) @asynccontextmanager async def get_db_async(): - engine = await make_async_engine(get_settings().ASYNC_DATABASE_PATH) + engine = await make_async_engine(get_settings().async_database_path) async_session = await make_async_session_local(engine) async with async_session() as session: - try: yield session - finally: await engine.dispose() + try: + yield session + finally: + await engine.dispose() diff --git a/app/shared/db/models.py b/app/shared/db/models.py index aa93034..ca58506 100644 --- a/app/shared/db/models.py +++ b/app/shared/db/models.py @@ -42,7 +42,9 @@ class Archive(Base): id = Column(String, primary_key=True, index=True) url = Column(String, index=True) result = Column(JSON, default=None) - public = Column(Boolean, default=True) # if public=false, access by group and author + public = Column( + Boolean, default=True + ) # if public=false, access by group and author deleted = Column(Boolean, default=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) @@ -52,7 +54,11 @@ class Archive(Base): author_id = Column(String, ForeignKey("users.email")) sheet_id = Column(String, ForeignKey("sheets.id"), default=None) - tags = relationship("Tag", back_populates="archives", secondary=association_table_archive_tags) + tags = relationship( + "Tag", + back_populates="archives", + secondary=association_table_archive_tags, + ) group = relationship("Group", back_populates="archives") author = relationship("User", back_populates="archives") urls = relationship("ArchiveUrl", back_populates="archive") @@ -75,7 +81,11 @@ class Tag(Base): id = Column(String, primary_key=True, index=True) created_at = Column(DateTime(timezone=True), server_default=func.now()) - archives = relationship("Archive", back_populates="tags", secondary=association_table_archive_tags) + archives = relationship( + "Archive", + back_populates="tags", + secondary=association_table_archive_tags, + ) class User(Base): @@ -85,7 +95,9 @@ class User(Base): archives = relationship("Archive", back_populates="author") sheets = relationship("Sheet", back_populates="author") - groups = relationship("Group", back_populates="users", secondary=association_table_user_groups) + groups = relationship( + "Group", back_populates="users", secondary=association_table_user_groups + ) class Group(Base): @@ -101,7 +113,9 @@ class Group(Base): archives = relationship("Archive", back_populates="group") sheets = relationship("Sheet", back_populates="group") - users = relationship("User", back_populates="groups", secondary=association_table_user_groups) + users = relationship( + "User", back_populates="groups", secondary=association_table_user_groups + ) class Sheet(Base): @@ -110,11 +124,27 @@ class Sheet(Base): id = Column(String, primary_key=True, index=True, doc="Google Sheet ID") name = Column(String, default=None) author_id = Column(String, ForeignKey("users.email")) - group_id = Column(String, ForeignKey("groups.id"), doc="Group ID, user must be in a group to create a sheet.") - frequency = Column(String, default="daily", doc="Frequency of archiving: hourly, daily, weekly.") + group_id = Column( + String, + ForeignKey("groups.id"), + doc="Group ID, user must be in a group to create a sheet.", + ) + frequency = Column( + String, + default="daily", + doc="Frequency of archiving: hourly, daily, weekly.", + ) # TODO: stats is not being used, consider removing - stats = Column(JSON, default={}, doc="Sheet statistics like total links, total rows, ...") - last_url_archived_at = Column(DateTime(timezone=True), server_default=func.now(), doc="Last time a new link was archived.") + stats = Column( + JSON, + default={}, + doc="Sheet statistics like total links, total rows, ...", + ) + last_url_archived_at = Column( + DateTime(timezone=True), + server_default=func.now(), + doc="Last time a new link was archived.", + ) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) diff --git a/app/shared/db/worker_crud.py b/app/shared/db/worker_crud.py index 82f3380..28d7af8 100644 --- a/app/shared/db/worker_crud.py +++ b/app/shared/db/worker_crud.py @@ -9,7 +9,9 @@ from app.shared.db import models # TODO: isolate database operations away from worker and into WEB # ONLY WORKER def update_sheet_last_url_archived_at(db: Session, sheet_id: str): - db_sheet = db.query(models.Sheet).filter(models.Sheet.id == sheet_id).first() + db_sheet = ( + db.query(models.Sheet).filter(models.Sheet.id == sheet_id).first() + ) if db_sheet: db_sheet.last_url_archived_at = datetime.now() db.commit() @@ -19,12 +21,17 @@ def update_sheet_last_url_archived_at(db: Session, sheet_id: str): # ONLY WORKER and INTEROP + def get_group(db: Session, group_name: str) -> models.Group: return db.query(models.Group).filter(models.Group.id == group_name).first() + def create_or_get_user(db: Session, author_id: str) -> models.User: - if type(author_id) == str: author_id = author_id.lower() - db_user = db.query(models.User).filter(models.User.email == author_id).first() + if isinstance(author_id, str): + author_id = author_id.lower() + db_user = ( + db.query(models.User).filter(models.User.email == author_id).first() + ) if not db_user: db_user = models.User(email=author_id) db.add(db_user) @@ -43,8 +50,22 @@ def create_tag(db: Session, tag: str) -> models.Tag: return db_tag -def create_archive(db: Session, archive: schemas.ArchiveCreate, tags: list[models.Tag], urls: list[models.ArchiveUrl]) -> models.Archive: - db_archive = models.Archive(id=archive.id, url=archive.url, result=archive.result, public=archive.public, author_id=archive.author_id, group_id=archive.group_id, sheet_id=archive.sheet_id, store_until=archive.store_until) +def create_archive( + db: Session, + archive: schemas.ArchiveCreate, + tags: list[models.Tag], + urls: list[models.ArchiveUrl], +) -> models.Archive: + db_archive = models.Archive( + id=archive.id, + url=archive.url, + result=archive.result, + public=archive.public, + author_id=archive.author_id, + group_id=archive.group_id, + sheet_id=archive.sheet_id, + store_until=archive.store_until, + ) db_archive.tags = tags db_archive.urls = urls db.add(db_archive) @@ -53,10 +74,14 @@ def create_archive(db: Session, archive: schemas.ArchiveCreate, tags: list[model return db_archive -def store_archived_url(db: Session, archive: schemas.ArchiveCreate) -> models.Archive: +def store_archived_url( + db: Session, archive: schemas.ArchiveCreate +) -> models.Archive: # create and load user, tags, if needed create_or_get_user(db, archive.author_id) db_tags = [create_tag(db, tag) for tag in (archive.tags or [])] # insert everything - db_archive = create_archive(db, archive=archive, tags=db_tags, urls=archive.urls) + db_archive = create_archive( + db, archive=archive, tags=db_tags, urls=archive.urls + ) return db_archive diff --git a/app/shared/log.py b/app/shared/log.py index 734c368..152bcc0 100644 --- a/app/shared/log.py +++ b/app/shared/log.py @@ -8,7 +8,9 @@ logger.add("logs/api_logs.log", retention="30 days") logger.add("logs/error_logs.log", retention="30 days", level="ERROR") -def log_error(e: Exception, traceback_str: str = None, extra:str = ""): - if not traceback_str: traceback_str = traceback.format_exc() - if extra: extra = f"{extra}\n" +def log_error(e: Exception, traceback_str: str = None, extra: str = ""): + if not traceback_str: + traceback_str = traceback.format_exc() + if extra: + extra = f"{extra}\n" logger.error(f"{extra}{e.__class__.__name__}: {e}\n{traceback_str}") diff --git a/app/shared/schemas.py b/app/shared/schemas.py index e8479f5..16dec0f 100644 --- a/app/shared/schemas.py +++ b/app/shared/schemas.py @@ -11,6 +11,7 @@ class SubmitSheet(BaseModel): group_id: str = "default" tags: set[str] | None = set() + class ArchiveUrl(BaseModel): url: str public: bool = False @@ -18,6 +19,7 @@ class ArchiveUrl(BaseModel): group_id: str | None tags: set[str] | None = set() + class ArchiveResult(BaseModel): id: str url: str diff --git a/app/shared/settings.py b/app/shared/settings.py index 2f68ae7..867bc84 100644 --- a/app/shared/settings.py +++ b/app/shared/settings.py @@ -1,4 +1,3 @@ - import os from functools import lru_cache from typing import Annotated, Set @@ -9,32 +8,40 @@ from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=os.environ.get("ENVIRONMENT_FILE"), + env_file_encoding="utf-8", + extra="ignore", + str_strip_whitespace=True, + ) - model_config = SettingsConfigDict(env_file=os.environ.get("ENVIRONMENT_FILE") , env_file_encoding='utf-8', extra='ignore', str_strip_whitespace=True) - - # general + # general SERVE_LOCAL_ARCHIVE: str | None = None USER_GROUPS_FILENAME: str = "app/user-groups.yaml" - # database + # database DATABASE_PATH: str DATABASE_QUERY_LIMIT: int = 100 + @property - def ASYNC_DATABASE_PATH(self) -> str: + def async_database_path(self) -> str: return self.DATABASE_PATH.replace("sqlite://", "sqlite+aiosqlite://") - # security + # security API_BEARER_TOKEN: Annotated[str, Len(min_length=20)] ALLOWED_ORIGINS: Annotated[Set[str], Len(min_length=1)] - CHROME_APP_IDS: Annotated[Set[Annotated[str, Len(min_length=10)]], Len(min_length=1)] + CHROME_APP_IDS: Annotated[ + Set[Annotated[str, Len(min_length=10)]], Len(min_length=1) + ] BLOCKED_EMAILS: Annotated[Set[str], Len(min_length=0)] = set() # redis REDIS_PASSWORD: str = "" REDIS_HOSTNAME: str = "localhost" REDIS_EXCEPTIONS_CHANNEL: str = "exceptions-channel" + @property - def CELERY_BROKER_URL(self)-> str: + def celery_broker_url(self) -> str: if self.REDIS_PASSWORD: return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOSTNAME}:6379" return f"redis://{self.REDIS_HOSTNAME}:6379" @@ -46,7 +53,7 @@ class Settings(BaseSettings): CRON_DELETE_SCHEDULED_ARCHIVES: bool = False DELETE_SCHEDULED_ARCHIVES_CHECK_EVERY_N_DAYS: int = 7 - # observability + # observability REPEAT_COUNT_METRICS_SECONDS: int = 30 # email configuration, if needed @@ -58,8 +65,9 @@ class Settings(BaseSettings): MAIL_PORT: int = 587 MAIL_STARTTLS: bool = False MAIL_SSL_TLS: bool = True + @property - def MAIL_CONFIG(self) -> str: + def mail_config(self) -> ConnectionConfig: return ConnectionConfig( MAIL_FROM=self.MAIL_FROM, MAIL_FROM_NAME=self.MAIL_FROM_NAME, diff --git a/app/shared/task_messaging.py b/app/shared/task_messaging.py index 88d0057..95438b5 100644 --- a/app/shared/task_messaging.py +++ b/app/shared/task_messaging.py @@ -1,4 +1,3 @@ - from functools import lru_cache from celery import Celery @@ -11,14 +10,14 @@ from app.shared.settings import get_settings def get_celery(name: str = "") -> Celery: return Celery( name, - broker_url=get_settings().CELERY_BROKER_URL, - result_backend=get_settings().CELERY_BROKER_URL, + broker_url=get_settings().celery_broker_url, + result_backend=get_settings().celery_broker_url, broker_connection_retry_on_startup=False, broker_transport_options={ - 'queue_order_strategy': 'priority', - } + "queue_order_strategy": "priority", + }, ) def get_redis() -> redis.Redis: - return redis.Redis.from_url(get_settings().CELERY_BROKER_URL) + return redis.Redis.from_url(get_settings().celery_broker_url) diff --git a/app/shared/user_groups.py b/app/shared/user_groups.py index 480b84e..764cbca 100644 --- a/app/shared/user_groups.py +++ b/app/shared/user_groups.py @@ -19,13 +19,16 @@ class UserGroups: user_groups = self.read_yaml(filename) self.validate_and_load(user_groups) - def read_yaml(self, user_groups_filename): + @staticmethod + def read_yaml(user_groups_filename): # read yaml safely with open(user_groups_filename) as inf: try: return yaml.safe_load(inf) except yaml.YAMLError as e: - logger.error(f"could not open user groups filename {user_groups_filename}: {e}") + logger.error( + f"could not open user groups filename {user_groups_filename}: {e}" + ) raise e def validate_and_load(self, user_groups): @@ -52,22 +55,36 @@ class GroupPermissions(BaseModel): max_monthly_mbs: int = 0 priority: str = "low" - @field_validator('max_sheets', 'max_archive_lifespan_months', 'max_monthly_urls', 'max_monthly_mbs', mode='before') + @classmethod + @field_validator( + "max_sheets", + "max_archive_lifespan_months", + "max_monthly_urls", + "max_monthly_mbs", + mode="before", + ) def validate_max_values(cls, v): if v < -1: - raise ValueError("max_* values should be positive integers or -1 (for no limit).") + raise ValueError( + "max_* values should be positive integers or -1 (for no limit)." + ) return v - @field_validator('sheet_frequency', mode='before') + @classmethod + @field_validator("sheet_frequency", mode="before") def validate_sheet_frequency(cls, v): - if not v: return [] + if not v: + return [] allowed = ["daily", "hourly"] for k in v: if k not in allowed: - raise ValueError(f"Invalid sheet frequency: '{k}', expected one of {allowed}") + raise ValueError( + f"Invalid sheet frequency: '{k}', expected one of {allowed}" + ) return v - @field_validator('priority', mode='before') + @classmethod + @field_validator("priority", mode="before") def validate_priority(cls, v): v = v.lower() if v not in ["low", "high"]: @@ -81,7 +98,8 @@ class GroupModel(BaseModel): orchestrator_sheet: str permissions: GroupPermissions - @field_validator('orchestrator', 'orchestrator_sheet', mode='before') + @classmethod + @field_validator("orchestrator", "orchestrator_sheet", mode="before") def validate_orchestrator(cls, v): if not os.path.exists(v): raise ValueError(f"Orchestrator file not found with this path: {v}") @@ -105,13 +123,17 @@ class GroupModel(BaseModel): service_account_json = find_service_account_email(orch) if not service_account_json: - raise ValueError(f"service_account key not found in orchestrator sheet file: {self.orchestrator_sheet}.") + raise ValueError( + f"service_account key not found in orchestrator sheet file: {self.orchestrator_sheet}." + ) with open(service_account_json) as f: self._service_account_email = json.load(f).get("client_email") if not self._service_account_email: - raise ValueError(f"Service account email not found in {service_account_json}.") + raise ValueError( + f"Service account email not found in {service_account_json}." + ) return self._service_account_email @@ -121,29 +143,45 @@ class UserGroupModel(BaseModel): domains: Dict[str, List[str]] = Field(default_factory=dict) groups: Dict[str, GroupModel] = Field(default_factory=dict) - @field_validator('users', mode='before') @classmethod + @field_validator("users", mode="before") def validate_emails(cls, v): for email in v.keys(): - if '@' not in email: - raise ValueError(f"Invalid user, it should be an address: {email}") + if "@" not in email: + raise ValueError( + f"Invalid user, it should be an address: {email}" + ) if not v[email]: - raise ValueError(f"User {email} has no explicitly listed groups, only include them here if they should be in a group.") + raise ValueError( + f"User {email} has no explicitly listed groups, only include them here if they should be in a group." + ) # all users belong to the default group - return {k.lower().strip(): list(set(["default"] + [g.lower().strip() for g in v])) for k, v in v.items()} + return { + k.lower().strip(): list( + set(["default"] + [g.lower().strip() for g in v]) + ) + for k, v in v.items() + } - @field_validator('domains', mode='before') @classmethod + @field_validator("domains", mode="before") def validate_domains(cls, v): for domain, members in v.items(): - if '.' not in domain: - raise ValueError(f"Invalid domain, it should contain a dot: {domain}") + if "." not in domain: + raise ValueError( + f"Invalid domain, it should contain a dot: {domain}" + ) if not members: - raise ValueError(f"Domain {domain} should have at least one member.") - return {k.lower().strip(): list(set([g.lower().strip() for g in v])) for k, v in v.items()} + raise ValueError( + f"Domain {domain} should have at least one member." + ) + return { + k.lower().strip(): list({[g.lower().strip() for g in v]}) + for k, v in v.items() + } - @field_validator('groups', mode='before') @classmethod + @field_validator("groups", mode="before") def validate_groups(cls, v): if "default" not in v.keys(): raise ValueError("Please include a 'default' group.") @@ -154,20 +192,28 @@ class UserGroupModel(BaseModel): raise ValueError(f"Group names should be lowercase: {group}") return v - @model_validator(mode='after') + @model_validator(mode="after") def check_groups_consistency(self) -> Self: - groups_in_domains = set([g for domain in self.domains for g in self.domains[domain]]) - groups_in_users = set([g for user in self.users for g in self.users[user]]) + groups_in_domains = { + g for domain in self.domains for g in self.domains[domain] + } + groups_in_users = {g for user in self.users for g in self.users[user]} configured_groups = set(self.groups.keys()) - # groups mentioned in domains and users should be defined, but this is not a ValueError since historical DB data may require it + # groups mentioned in domains and users should be defined, but this is + # not a ValueError since historical DB data may require it if groups_in_domains - configured_groups: - logger.warning(f"These groups are associated to DOMAINS but not defined in the GROUPS section, the domains settings may not work as expected: {groups_in_domains - configured_groups}") + logger.warning( + f"These groups are associated to DOMAINS but not defined in the GROUPS section, the domains settings may not work as expected: {groups_in_domains - configured_groups}" + ) if groups_in_users - configured_groups: - logger.warning(f"These groups are associated to USERS but not defined in the GROUPS section, the users settings may not work as expected: {groups_in_users - configured_groups}") + logger.warning( + f"These groups are associated to USERS but not defined in the GROUPS section, the users settings may not work as expected: {groups_in_users - configured_groups}" + ) return self + # for the API return values diff --git a/app/shared/utils/misc.py b/app/shared/utils/misc.py index 6c5940d..21e349a 100644 --- a/app/shared/utils/misc.py +++ b/app/shared/utils/misc.py @@ -1,10 +1,14 @@ - -def fnv1a_hash_mod(s: str, modulo:int) -> int: - # receives a string and returns a number in [0:modulo-1], ensures an even distribution over the modulo range - hash = 0x811c9dc5 # FNV offset basis - fnv_prime = 0x01000193 # FNV prime +def fnv1a_hash_mod(s: str, modulo: int) -> int: + # receives a string and returns a number in [0:modulo-1], ensures an even + # distribution over the modulo range + offset_basis_hash = 0x811C9DC5 # FNV offset basis + fnv_prime = 0x01000193 # FNV prime for char in s: - hash ^= ord(char) - hash *= fnv_prime - hash &= 0xFFFFFFFF # Keep it 32-bit - return (hash if hash < 0x80000000 else hash - 0x100000000) % modulo + offset_basis_hash ^= ord(char) + offset_basis_hash *= fnv_prime + offset_basis_hash &= 0xFFFFFFFF # Keep it 32-bit + return ( + offset_basis_hash + if offset_basis_hash < 0x80000000 + else offset_basis_hash - 0x100000000 + ) % modulo diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 997ea62..c41d249 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -84,9 +84,9 @@ def db_session(test_db): @pytest_asyncio.fixture() async def async_test_db(get_settings: Settings): get_user_group_names.cache_clear() - engine = await make_async_engine(get_settings.ASYNC_DATABASE_PATH) + engine = await make_async_engine(get_settings.async_database_path) - fs = get_settings.ASYNC_DATABASE_PATH.replace("sqlite+aiosqlite:///", "") + fs = get_settings.async_database_path.replace("sqlite+aiosqlite:///", "") if not os.path.exists(fs): open(fs, "w").close() diff --git a/app/web/events.py b/app/web/events.py index 17a24b9..e9af845 100644 --- a/app/web/events.py +++ b/app/web/events.py @@ -160,7 +160,7 @@ async def notify_about_expired_archives(): user_archives[archive.author_id].append(archive) if user_archives: - fastmail = FastMail(get_settings().MAIL_CONFIG) + fastmail = FastMail(get_settings().mail_config) # notify users for email in user_archives: list_of_archives = "\n".join( @@ -224,7 +224,7 @@ async def delete_stale_sheets(): if not user_sheets: return - fastmail = FastMail(get_settings().MAIL_CONFIG) + fastmail = FastMail(get_settings().mail_config) # notify users for email in user_sheets: list_of_sheets = "\n".join( From b50ca91d8966153c3b110c70642b92dd9e4e6973 Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:45:19 -0500 Subject: [PATCH 15/40] Format and lint `web` directory (#67) --- .pre-commit-config.yaml | 30 +-- app/migrations/env.py | 4 +- app/shared/db/models.py | 2 +- app/tests/shared/db/test_worker_crud.py | 24 +-- app/tests/web/db/test_crud.py | 6 +- app/tests/web/test_main.py | 2 +- app/web/config.py | 5 +- app/web/db/crud.py | 256 +++++++++++++++++----- app/web/db/user_state.py | 272 ++++++++++++++++-------- app/web/endpoints/default.py | 34 ++- app/web/endpoints/interoperability.py | 39 +++- app/web/endpoints/sheet.py | 115 +++++++--- app/web/endpoints/task.py | 32 +-- app/web/endpoints/url.py | 98 ++++++--- app/web/main.py | 41 +++- app/web/middleware.py | 18 +- app/web/security.py | 51 +++-- app/web/utils/metrics.py | 34 +-- app/web/utils/misc.py | 4 +- pyproject.toml | 3 + 20 files changed, 761 insertions(+), 309 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index edb6bcf..6707b21 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,18 +61,18 @@ repos: - --profile=black - --line-length=80 -# - repo: https://github.com/astral-sh/ruff-pre-commit -# rev: v0.9.7 -# hooks: -# - id: ruff -# types_or: [python,pyi] -# args: -# - --fix -# - --select=B,C,E,F,W,B9 -# - --line-length=80 -# - --ignore=E203,E402,E501,E261 -# - id: ruff-format -# types_or: [ python,pyi] -# args: -# - --target-version=py310 -# - --line-length=80 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.7 + hooks: + - id: ruff + types_or: [python,pyi] + args: + - --fix + - --select=B,C,E,F,W,B9 + - --line-length=80 + - --ignore=E203,E402,E501,E261 + - id: ruff-format + types_or: [ python,pyi] + args: + - --target-version=py310 + - --line-length=80 diff --git a/app/migrations/env.py b/app/migrations/env.py index 54c1f48..fc63a92 100644 --- a/app/migrations/env.py +++ b/app/migrations/env.py @@ -14,9 +14,7 @@ config.set_main_option("sqlalchemy.url", get_settings().DATABASE_PATH) # This line sets up loggers basically. if config.config_file_name is not None: # disable_existing_loggers prevents loguru disabling - fileConfig( - config.config_file_name, disable_existing_loggers=False - ) + fileConfig(config.config_file_name, disable_existing_loggers=False) # add your model's MetaData object here # for 'autogenerate' support diff --git a/app/shared/db/models.py b/app/shared/db/models.py index ca58506..8acedc1 100644 --- a/app/shared/db/models.py +++ b/app/shared/db/models.py @@ -20,7 +20,7 @@ def generate_uuid(): return str(uuid.uuid4()) -# many to many association tables +# many-to-many association tables association_table_archive_tags = Table( "mtm_archives_tags", Base.metadata, diff --git a/app/tests/shared/db/test_worker_crud.py b/app/tests/shared/db/test_worker_crud.py index 2258781..4e5a434 100644 --- a/app/tests/shared/db/test_worker_crud.py +++ b/app/tests/shared/db/test_worker_crud.py @@ -14,8 +14,8 @@ def test_update_sheet_last_url_archived_at(db_session): assert isinstance(test_sheet.last_url_archived_at, datetime) before = test_sheet.last_url_archived_at assert ( - worker_crud.update_sheet_last_url_archived_at(db_session, "sheet-123") - is True + worker_crud.update_sheet_last_url_archived_at(db_session, "sheet-123") + is True ) db_session.refresh(test_sheet) assert isinstance(test_sheet.last_url_archived_at, datetime) @@ -23,10 +23,10 @@ def test_update_sheet_last_url_archived_at(db_session): # Test non-existent sheet assert ( - worker_crud.update_sheet_last_url_archived_at( - db_session, "non-existent-sheet" - ) - is False + worker_crud.update_sheet_last_url_archived_at( + db_session, "non-existent-sheet" + ) + is False ) @@ -42,14 +42,14 @@ def test_create_or_get_user(test_data, db_session): # already exists assert ( - u1 := worker_crud.create_or_get_user(db_session, "rick@example.com") - ) is not None + u1 := worker_crud.create_or_get_user(db_session, "rick@example.com") + ) is not None assert u1.email == "rick@example.com" # new user assert ( - u2 := worker_crud.create_or_get_user(db_session, "beth@example.com") - ) is not None + u2 := worker_crud.create_or_get_user(db_session, "beth@example.com") + ) is not None assert u2.email == "beth@example.com" assert db_session.query(models.User).count() == 4 @@ -64,8 +64,8 @@ def test_create_tag(db_session): assert create_tag.id == "tag-101" assert db_session.query(models.Tag).count() == 1 assert ( - db_session.query(models.Tag).filter(models.Tag.id == "tag-101").first() - == create_tag + db_session.query(models.Tag).filter(models.Tag.id == "tag-101").first() + == create_tag ) # same id does not add new db entry diff --git a/app/tests/web/db/test_crud.py b/app/tests/web/db/test_crud.py index 676aa24..7b569b6 100644 --- a/app/tests/web/db/test_crud.py +++ b/app/tests/web/db/test_crud.py @@ -712,7 +712,11 @@ async def test_find_by_store_until(async_db_session): @pytest.mark.asyncio async def test_get_sheets_by_id_hash(async_db_session): - author_emails = ["rick@example.com", "morty@example.com", "jerry@example.com"] + author_emails = [ + "rick@example.com", + "morty@example.com", + "jerry@example.com", + ] # Add test data sheets = [ diff --git a/app/tests/web/test_main.py b/app/tests/web/test_main.py index eb985a8..1b6e86b 100644 --- a/app/tests/web/test_main.py +++ b/app/tests/web/test_main.py @@ -7,6 +7,7 @@ import alembic.config import pytest from fastapi.testclient import TestClient +from app.web.main import app_factory from app.web.utils.metrics import EXCEPTION_COUNTER @@ -59,7 +60,6 @@ def test_serve_local_archive_logic(get_settings): try: # modify the settings get_settings.SERVE_LOCAL_ARCHIVE = "/app/local_archive_test" - from app.web.main import app_factory app = app_factory(get_settings) diff --git a/app/web/config.py b/app/web/config.py index b795d88..359a9a8 100644 --- a/app/web/config.py +++ b/app/web/config.py @@ -8,7 +8,10 @@ API_DESCRIPTION = """ - You can use this API to archive single URLs or entire Google Sheets. - Once you submit a URL or Sheet for archiving, the API will return a task_id that you can use to check the status of the archiving process. It works asynchronously. """ -BREAKING_CHANGES = {"minVersion": "0.4.0", "message": "The latest update has breaking changes, please update the extension to the most recent version."} +BREAKING_CHANGES = { + "minVersion": "0.4.0", + "message": "The latest update has breaking changes, please update the extension to the most recent version.", +} # changing this will corrupt the database logic ALLOW_ANY_EMAIL = "*" diff --git a/app/web/db/crud.py b/app/web/db/crud.py index b33faa2..308c526 100644 --- a/app/web/db/crud.py +++ b/app/web/db/crud.py @@ -1,15 +1,26 @@ from collections import defaultdict from datetime import datetime, timedelta -from functools import lru_cache +from typing import Any, Type from cachetools import LRUCache, cached from cachetools.keys import hashkey from loguru import logger -from sqlalchemy import Column, func, or_, select +from sqlalchemy import ( + Column, + ColumnElement, + ScalarResult, + false, + func, + not_, + or_, + select, + true, +) from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session, load_only from app.shared.db import models +from app.shared.db.models import Archive, Group from app.shared.settings import get_settings from app.shared.user_groups import UserGroups from app.shared.utils.misc import fnv1a_hash_mod @@ -23,24 +34,48 @@ DATABASE_QUERY_LIMIT = get_settings().DATABASE_QUERY_LIMIT def get_limit(user_limit: int): return max(1, min(user_limit, DATABASE_QUERY_LIMIT)) + # --------------- TASK = Archive def base_query(db: Session): - # NOTE: load_only is for optimization and not obfuscation, use .with_entities() if needed - return db.query(models.Archive)\ - .filter(models.Archive.deleted == False)\ - .options(load_only(models.Archive.id, models.Archive.created_at, models.Archive.url, models.Archive.result, models.Archive.store_until)) + # NOTE: load_only is for optimization and not obfuscation, use + # .with_entities() if needed + return ( + db.query(models.Archive) + .filter(not_(models.Archive.deleted)) + .options( + load_only( + models.Archive.id, + models.Archive.created_at, + models.Archive.url, + models.Archive.result, + models.Archive.store_until, + ) + ) + ) -def search_archives_by_url(db: Session, url: str, email: str, read_groups: bool | set[str], read_public: bool, skip: int = 0, limit: int = 100, archived_after: datetime = None, archived_before: datetime = None, absolute_search: bool = False) -> list[models.Archive]: - # searches for partial URLs, if email is * no ownership (or read/read_public) filtering happens +def search_archives_by_url( + db: Session, + url: str, + email: str, + read_groups: bool | set[str], + read_public: bool, + skip: int = 0, + limit: int = 100, + archived_after: datetime = None, + archived_before: datetime = None, + absolute_search: bool = False, +) -> list[Type[Archive]]: + # searches for partial URLs, if email is * no ownership + # (or read/read_public) filtering happens query = base_query(db) if email != ALLOW_ANY_EMAIL: or_filters = [models.Archive.author_id == email] if read_public: - or_filters.append(models.Archive.public == True) - if read_groups == True: + or_filters.append(models.Archive.public.is_(true())) + if read_groups is True: or_filters.append(models.Archive.group_id.isnot(None)) else: or_filters.append(models.Archive.group_id.in_(read_groups)) @@ -48,21 +83,43 @@ def search_archives_by_url(db: Session, url: str, email: str, read_groups: bool if absolute_search: query = query.filter(models.Archive.url == url) else: - query = query.filter(models.Archive.url.like(f'%{url}%')) + query = query.filter(models.Archive.url.like(f"%{url}%")) if archived_after: query = query.filter(models.Archive.created_at > archived_after) if archived_before: query = query.filter(models.Archive.created_at < archived_before) - return query.order_by(models.Archive.created_at.desc()).offset(skip).limit(get_limit(limit)).all() + return ( + query.order_by(models.Archive.created_at.desc()) + .offset(skip) + .limit(get_limit(limit)) + .all() + ) -def search_archives_by_email(db: Session, email: str, skip: int = 0, limit: int = 100): - return base_query(db).filter(models.Archive.author_id == email).order_by(models.Archive.created_at.desc()).offset(skip).limit(get_limit(limit)).all() +def search_archives_by_email( + db: Session, email: str, skip: int = 0, limit: int = 100 +): + return ( + base_query(db) + .filter(models.Archive.author_id == email) + .order_by(models.Archive.created_at.desc()) + .offset(skip) + .limit(get_limit(limit)) + .all() + ) def soft_delete_archive(db: Session, id: str, email: str) -> bool: # TODO: implement hard-delete with cronjob that deletes from S3 - db_archive = db.query(models.Archive).filter(models.Archive.id == id, models.Archive.author_id == email, models.Archive.deleted == False).first() + db_archive = ( + db.query(models.Archive) + .filter( + models.Archive.id == id, + models.Archive.author_id == email, + models.Archive.deleted.is_(false()), + ) + .first() + ) if db_archive: db_archive.deleted = True db.commit() @@ -83,22 +140,29 @@ def count_users(db: Session): def count_by_user_since(db: Session, seconds_delta: int = 15): time_threshold = datetime.now() - timedelta(seconds=seconds_delta) - return db.query(models.Archive.author_id, func.count().label('total'))\ - .filter(models.Archive.created_at >= time_threshold)\ - .group_by(models.Archive.author_id)\ - .order_by(func.count().desc())\ - .limit(500).all() + return ( + db.query(models.Archive.author_id, func.count().label("total")) + .filter(models.Archive.created_at >= time_threshold) + .group_by(models.Archive.author_id) + .order_by(func.count().desc()) + .limit(500) + .all() + ) -async def find_by_store_until(db: AsyncSession, store_until_is_before: datetime) -> list[models.Archive]: +async def find_by_store_until( + db: AsyncSession, store_until_is_before: datetime +) -> ScalarResult[Archive]: res = await db.execute( - select(models.Archive) - .filter(models.Archive.deleted == False, models.Archive.store_until < store_until_is_before) + select(models.Archive).filter( + models.Archive.deleted.is_(false()), + models.Archive.store_until < store_until_is_before, + ) ) return res.scalars() -async def soft_delete_expired_archives(db: AsyncSession) -> dict: +async def soft_delete_expired_archives(db: AsyncSession) -> int: to_delete = await find_by_store_until(db, datetime.now()) counter = 0 for archive in to_delete: @@ -106,47 +170,86 @@ async def soft_delete_expired_archives(db: AsyncSession) -> dict: counter += 1 await db.commit() return counter + + # --------------- TAG async def get_group_priority_async(db: AsyncSession, group_id: str) -> dict: db_group = await db.get(models.Group, group_id) - priority = db_group.permissions.get("priority", "low") if db_group else "low" + priority = ( + db_group.permissions.get("priority", "low") if db_group else "low" + ) return convert_priority_to_queue_dict(priority) @cached(cache=LRUCache(maxsize=128), key=lambda db, email: hashkey(email)) -def get_user_group_names(db: Session, email: str) -> list[str]: +def get_user_group_names( + db: Session, email: str +) -> list[Any] | list[ColumnElement[Any]]: """ - given an email retrieves the user groups from the DB and then the email-domain groups from a global variable, the email does not need to belong to an existing user. + given an email retrieves the user groups from the DB and then the + email-domain groups from a global variable, the email does not need to + belong to an existing user. """ # TODO: the read: [group1, group2] permissions don't currently work - if not email or not len(email) or "@" not in email: return [] + if not email or not len(email) or "@" not in email: + return [] # get user groups - user_groups = db.query(models.association_table_user_groups).filter_by(user_id=email).with_entities(Column("group_id")).all() + user_groups = ( + db.query(models.association_table_user_groups) + .filter_by(user_id=email) + .with_entities(Column("group_id")) + .all() + ) user_level_groups_names = [g[0] for g in user_groups] # get domain groups - domain = email.split('@')[1] - domain_level_groups = db.query(models.Group.id).filter(models.Group.domains.contains(domain)).with_entities(Column("id")).all() + domain = email.split("@")[1] + domain_level_groups = ( + db.query(models.Group.id) + .filter(models.Group.domains.contains(domain)) + .with_entities(Column("id")) + .all() + ) domain_level_groups_names = [g[0] for g in domain_level_groups] return list(set(user_level_groups_names + domain_level_groups_names)) -def get_user_groups_by_name(db: Session, groups: list[str]) -> list[models.Group]: - return db.query(models.Group).filter( - models.Group.id.in_(groups) - ).all() +def get_user_groups_by_name( + db: Session, groups: list[str] +) -> list[Type[Group]]: + return db.query(models.Group).filter(models.Group.id.in_(groups)).all() + # --------------- INIT User-Groups -def upsert_group(db: Session, group_name: str, description: str, orchestrator: str, orchestrator_sheet: str, service_account_email: str, permissions: dict, domains: list) -> models.Group: - db_group = db.query(models.Group).filter(models.Group.id == group_name).first() +def upsert_group( + db: Session, + group_name: str, + description: str, + orchestrator: str, + orchestrator_sheet: str, + service_account_email: str, + permissions: dict, + domains: list, +) -> models.Group: + db_group = ( + db.query(models.Group).filter(models.Group.id == group_name).first() + ) if db_group is None: - db_group = models.Group(id=group_name, description=description, orchestrator=orchestrator, orchestrator_sheet=orchestrator_sheet, service_account_email=service_account_email, permissions=permissions, domains=domains) + db_group = models.Group( + id=group_name, + description=description, + orchestrator=orchestrator, + orchestrator_sheet=orchestrator_sheet, + service_account_email=service_account_email, + permissions=permissions, + domains=domains, + ) db.add(db_group) else: db_group.description = description @@ -173,6 +276,7 @@ def upsert_user(db: Session, email: str): def upsert_user_groups(db: Session): def display_email_pii(email: str): return f"'{email[0:3]}...@{email.split('@')[1]}'" + """ reads the user_groups yaml file and inserts any new users, groups, along with new participation of users in groups @@ -196,16 +300,30 @@ def upsert_user_groups(db: Session): # upsert groups and save a map of groupid -> dbobject for group_id, g in ug.groups.items(): - upsert_group(db, group_id, g.description, g.orchestrator, g.orchestrator_sheet, g.service_account_email, json.loads(g.permissions.model_dump_json()), list(group_domains.get(group_id, []))) - db_groups: dict[str, models.Group] = {g.id: g for g in db.query(models.Group).all()} + upsert_group( + db, + group_id, + g.description, + g.orchestrator, + g.orchestrator_sheet, + g.service_account_email, + json.loads(g.permissions.model_dump_json()), + list(group_domains.get(group_id, [])), + ) + db_groups: dict[str, models.Group] = { + g.id: g for g in db.query(models.Group).all() + } # integrity checks for group_in_domains in group_domains: if group_in_domains not in db_groups: - logger.warning(f"[CONFIG] Group '{group_in_domains}' does not exist in the database: domains setting will not work.") + logger.warning( + f"[CONFIG] Group '{group_in_domains}' does not exist in the database: domains setting will not work." + ) # reinsert users in their EXPLICITLY DEFINED groups - # domain groups are check live, as there may be new users that are not explicitly registered but belong to a domain + # domain groups are check live, as there may be new users that are not + # explicitly registered but belong to a domain for email, explicit_groups in ug.users.items(): explicit_groups = explicit_groups or [] logger.info(f"EXPLICIT {display_email_pii(email)} => {explicit_groups}") @@ -215,7 +333,9 @@ def upsert_user_groups(db: Session): # connect users to groups for group_id in explicit_groups: if group_id not in db_groups: - logger.warning(f"[CONFIG] Group {group_id} does not exist in config file, skipping for email={display_email_pii(email)}.") + logger.warning( + f"[CONFIG] Group {group_id} does not exist in config file, skipping for email={display_email_pii(email)}." + ) continue db_groups[group_id].users.append(db_user) @@ -223,12 +343,27 @@ def upsert_user_groups(db: Session): count_user_groups = db.query(models.association_table_user_groups).count() count_groups = db.query(func.count(models.Group.id)).scalar() - logger.success(f"[CONFIG] DONE: [users={count_users(db)}, groups={count_groups}, explicit user groups={count_user_groups}].") + logger.success( + f"[CONFIG] DONE: [users={count_users(db)}, groups={count_groups}, explicit user groups={count_user_groups}]." + ) # --------------- SHEET -def create_sheet(db: Session, sheet_id: str, name: str, email: str, group_id: str, frequency: str): - db_sheet = models.Sheet(id=sheet_id, name=name, author_id=email, group_id=group_id, frequency=frequency) +def create_sheet( + db: Session, + sheet_id: str, + name: str, + email: str, + group_id: str, + frequency: str, +): + db_sheet = models.Sheet( + id=sheet_id, + name=name, + author_id=email, + group_id=group_id, + frequency=frequency, + ) db.add(db_sheet) db.commit() db.refresh(db_sheet) @@ -236,20 +371,31 @@ def create_sheet(db: Session, sheet_id: str, name: str, email: str, group_id: st def get_user_sheet(db: Session, email: str, sheet_id: str) -> models.Sheet: - return db.query(models.Sheet).filter(models.Sheet.author_id == email, models.Sheet.id == sheet_id).first() + return ( + db.query(models.Sheet) + .filter(models.Sheet.author_id == email, models.Sheet.id == sheet_id) + .first() + ) def get_user_sheets(db: Session, email: str) -> list[models.Sheet]: - return db.query(models.Sheet).filter(models.Sheet.author_id == email).order_by(models.Sheet.last_url_archived_at.desc()).all() + return ( + db.query(models.Sheet) + .filter(models.Sheet.author_id == email) + .order_by(models.Sheet.last_url_archived_at.desc()) + .all() + ) -async def get_sheets_by_id_hash(db: AsyncSession, frequency: str, modulo: str, id_hash: int) -> list[models.Sheet]: +async def get_sheets_by_id_hash( + db: AsyncSession, frequency: str, modulo: str, id_hash: int +) -> list[models.Sheet]: result = await db.execute( select(models.Sheet).filter(models.Sheet.frequency == frequency) ) filtered = [] for sheet in result.scalars(): - if fnv1a_hash_mod(sheet.id, modulo) == id_hash: + if fnv1a_hash_mod(sheet.id, int(modulo)) == id_hash: filtered.append(sheet) return filtered @@ -257,7 +403,9 @@ async def get_sheets_by_id_hash(db: AsyncSession, frequency: str, modulo: str, i async def delete_stale_sheets(db: AsyncSession, inactivity_days: int) -> dict: time_threshold = datetime.now() - timedelta(days=inactivity_days) result = await db.execute( - select(models.Sheet).filter(models.Sheet.last_url_archived_at < time_threshold) + select(models.Sheet).filter( + models.Sheet.last_url_archived_at < time_threshold + ) ) deleted = defaultdict(list) for sheet in result.scalars(): @@ -268,7 +416,11 @@ async def delete_stale_sheets(db: AsyncSession, inactivity_days: int) -> dict: def delete_sheet(db: Session, sheet_id: str, email: str) -> bool: - db_sheet = db.query(models.Sheet).filter(models.Sheet.id == sheet_id, models.Sheet.author_id == email).first() + db_sheet = ( + db.query(models.Sheet) + .filter(models.Sheet.id == sheet_id, models.Sheet.author_id == email) + .first() + ) if db_sheet: db.delete(db_sheet) db.commit() diff --git a/app/web/db/user_state.py b/app/web/db/user_state.py index 384b0b6..67160db 100644 --- a/app/web/db/user_state.py +++ b/app/web/db/user_state.py @@ -1,4 +1,3 @@ - from datetime import datetime from typing import Dict, Set @@ -21,14 +20,15 @@ class UserState: def __init__(self, db: Session, email: str): self.db = db self.email = email.lower() + self._permissions = {} @property def permissions(self) -> Dict[str, GroupInfo]: """ - Returns a dict of all group permissions and a special {"all": read/archive_url/archive_sheet} key + Returns a dict of all group permissions and a special + {"all": read/archive_url/archive_sheet} key """ - if not hasattr(self, '_permissions'): - self._permissions = {} + if not self._permissions: self._permissions["all"] = GroupInfo( read=self.read, read_public=self.read_public, @@ -38,23 +38,33 @@ class UserState: max_archive_lifespan_months=self.max_archive_lifespan_months, max_monthly_urls=self.max_monthly_urls, max_monthly_mbs=self.max_monthly_mbs, - priority=self.priority + priority=self.priority, ) for group in self.user_groups: - if not group.permissions: continue - self._permissions[group.id] = GroupInfo(**group.permissions, description=group.description, service_account_email=group.service_account_email) + if not group.permissions: + continue + self._permissions[group.id] = GroupInfo( + **group.permissions, + description=group.description, + service_account_email=group.service_account_email, + ) return self._permissions @property def user_groups_names(self): - if not hasattr(self, '_user_groups_names'): - self._user_groups_names = crud.get_user_group_names(self.db, self.email) + ["default"] + if not hasattr(self, "_user_groups_names"): + # TODO: Define hidden properties in __init__ method + self._user_groups_names = crud.get_user_group_names( + self.db, self.email + ) + ["default"] return self._user_groups_names @property def user_groups(self): - if not hasattr(self, '_user_groups'): - self._user_groups = crud.get_user_groups_by_name(self.db, self.user_groups_names) + if not hasattr(self, "_user_groups"): + self._user_groups = crud.get_user_groups_by_name( + self.db, self.user_groups_names + ) return self._user_groups @property @@ -62,10 +72,11 @@ class UserState: """ Read can be a list of group names or True, if all can be read. """ - if not hasattr(self, '_read'): + if not hasattr(self, "_read"): self._read = set() for group in self.user_groups: - if not group.permissions: continue + if not group.permissions: + continue group_read_permissions = group.permissions.get("read", []) if "all" in group_read_permissions: self._read = True @@ -79,10 +90,11 @@ class UserState: """ Read public permission """ - if not hasattr(self, '_read_public'): + if not hasattr(self, "_read_public"): self._read_public = False for group in self.user_groups: - if not group.permissions: continue + if not group.permissions: + continue if group.permissions.get("read_public", False): self._read_public = True return self._read_public @@ -93,10 +105,11 @@ class UserState: """ Archive URL permission """ - if not hasattr(self, '_archive_url'): + if not hasattr(self, "_archive_url"): self._archive_url = False for group in self.user_groups: - if not group.permissions: continue + if not group.permissions: + continue if group.permissions.get("archive_url", False): self._archive_url = True return self._archive_url @@ -107,10 +120,11 @@ class UserState: """ Archive sheet permission """ - if not hasattr(self, '_archive_sheet'): + if not hasattr(self, "_archive_sheet"): self._archive_sheet = False for group in self.user_groups: - if not group.permissions: continue + if not group.permissions: + continue if group.permissions.get("archive_sheet", False): self._archive_sheet = True return self._archive_sheet @@ -118,37 +132,53 @@ class UserState: @property def sheet_frequency(self): - if not hasattr(self, '_sheet_frequency'): + if not hasattr(self, "_sheet_frequency"): self._sheet_frequency = set() for group in self.user_groups: - if not group.permissions: continue - self._sheet_frequency.update(group.permissions.get("sheet_frequency", None)) + if not group.permissions: + continue + self._sheet_frequency.update( + group.permissions.get("sheet_frequency", None) + ) return self._sheet_frequency @property def max_archive_lifespan_months(self) -> int: - if not hasattr(self, '_max_archive_lifespan_months'): - self._max_archive_lifespan_months = self._helper_for_grouping_max_numerical_permissions("max_archive_lifespan_months") + if not hasattr(self, "_max_archive_lifespan_months"): + self._max_archive_lifespan_months = ( + self._helper_for_grouping_max_numerical_permissions( + "max_archive_lifespan_months" + ) + ) return self._max_archive_lifespan_months @property def max_monthly_urls(self) -> int: - if not hasattr(self, '_max_monthly_urls'): - self._max_monthly_urls = self._helper_for_grouping_max_numerical_permissions("max_monthly_urls") + if not hasattr(self, "_max_monthly_urls"): + self._max_monthly_urls = ( + self._helper_for_grouping_max_numerical_permissions( + "max_monthly_urls" + ) + ) return self._max_monthly_urls @property def max_monthly_mbs(self) -> int: - if not hasattr(self, '_max_monthly_mbs'): - self._max_monthly_mbs = self._helper_for_grouping_max_numerical_permissions("max_monthly_mbs") + if not hasattr(self, "_max_monthly_mbs"): + self._max_monthly_mbs = ( + self._helper_for_grouping_max_numerical_permissions( + "max_monthly_mbs" + ) + ) return self._max_monthly_mbs @property def priority(self) -> str: - if not hasattr(self, '_priority'): + if not hasattr(self, "_priority"): self._priority = "low" for group in self.user_groups: - if not group.permissions: continue + if not group.permissions: + continue if group.permissions.get("priority", self._priority) == "high": self._priority = "high" break @@ -159,18 +189,28 @@ class UserState: """ A user is active if they can read/archive anything """ - if not hasattr(self, '_active'): - self._active = bool(self.read or self.read_public or self.archive_url or self.archive_sheet) + if not hasattr(self, "_active"): + self._active = bool( + self.read + or self.read_public + or self.archive_url + or self.archive_sheet + ) return self._active - def _helper_for_grouping_max_numerical_permissions(self, permission_name: str) -> int: + def _helper_for_grouping_max_numerical_permissions( + self, permission_name: str + ) -> int: """ - Iterates one of the numerical permissions where -1 means no restrictions and returns either -1 or the maximum value, defaults according to GroupPermissions + Iterates one of the numerical permissions where -1 means no restrictions + and returns either -1 or the maximum value, defaults according to + GroupPermissions """ default = GroupPermissions.model_fields[permission_name].default max_value = default for group in self.user_groups: - if not group.permissions: continue + if not group.permissions: + continue group_value = group.permissions.get(permission_name, default) if group_value == -1: max_value = -1 @@ -181,43 +221,65 @@ class UserState: def in_group(self, group_id: str) -> bool: return group_id in self.user_groups_names - def usage(self) -> Dict: + def usage(self) -> UsageResponse: """ - returns the monthly quotas for the URLs/MBs and the totals for Sheets + Returns the monthly quotas for the URLs/MBs and the totals for Sheets """ current_month = datetime.now().month current_year = datetime.now().year # find and sum all user sheets over this month - user_sheets = self.db.query( - models.Sheet.group_id, - func.count(models.Sheet.id).label('sheet_count') - ).filter(models.Sheet.author_id == self.email).group_by(models.Sheet.group_id).all() + user_sheets = ( + self.db.query( + models.Sheet.group_id, + func.count(models.Sheet.id).label("sheet_count"), + ) + .filter(models.Sheet.author_id == self.email) + .group_by(models.Sheet.group_id) + .all() + ) - sheets_by_group = {sheet.group_id: sheet.sheet_count for sheet in user_sheets} + sheets_by_group = { + sheet.group_id: sheet.sheet_count for sheet in user_sheets + } # find and sum all user urls over this month - urls_by_group = self.db.query( - models.Archive.group_id, - func.count(models.Archive.id).label('url_count'), - func.coalesce(func.sum( + urls_by_group = ( + self.db.query( + models.Archive.group_id, + func.count(models.Archive.id).label("url_count"), func.coalesce( - func.cast( - func.json_extract(models.Archive.result, '$.metadata.total_bytes'), - sqlalchemy.Integer - ), 0 - ) - ), 0).label('total_bytes') - ).filter( - models.Archive.author_id == self.email, - func.extract('month', models.Archive.created_at) == current_month, - func.extract('year', models.Archive.created_at) == current_year - ).group_by(models.Archive.group_id).all() + func.sum( + func.coalesce( + func.cast( + func.json_extract( + models.Archive.result, + "$.metadata.total_bytes", + ), + sqlalchemy.Integer, + ), + 0, + ) + ), + 0, + ).label("total_bytes"), + ) + .filter( + models.Archive.author_id == self.email, + func.extract("month", models.Archive.created_at) + == current_month, + func.extract("year", models.Archive.created_at) == current_year, + ) + .group_by(models.Archive.group_id) + .all() + ) # merge the two queries usage_by_group: Dict[str, Usage] = { - (url.group_id or ""): - Usage(monthly_urls=url.url_count, monthly_mbs=int(url.total_bytes / 1024 / 1024)) + (url.group_id or ""): Usage( + monthly_urls=url.url_count, + monthly_mbs=int(url.total_bytes / 1024 / 1024), + ) for url in urls_by_group } for group_id, sheet_count in sheets_by_group.items(): @@ -236,7 +298,7 @@ class UserState: monthly_urls=total_urls, monthly_mbs=int(total_bytes / 1024 / 1024), total_sheets=total_sheets, - groups=usage_by_group + groups=usage_by_group, ) def has_quota_monthly_sheets(self, group_id: str) -> bool: @@ -246,7 +308,14 @@ class UserState: if group_id not in self.permissions: return False - user_sheets = self.db.query(models.Sheet).filter(models.Sheet.author_id == self.email, models.Sheet.group_id == group_id).count() + user_sheets = ( + self.db.query(models.Sheet) + .filter( + models.Sheet.author_id == self.email, + models.Sheet.group_id == group_id, + ) + .count() + ) sheet_quota = self.permissions[group_id].max_sheets if sheet_quota == -1: @@ -255,13 +324,15 @@ class UserState: def has_quota_max_monthly_urls(self, group_id: str) -> bool: """ - checks if a user has reached their monthly url quota for a group, if global then group should be empty string + Checks if a user has reached their monthly url quota for a group, if + global then group should be empty string """ quota = 0 if not group_id: quota = self.max_monthly_urls else: - if group_id not in self.permissions: return False + if group_id not in self.permissions: + return False quota = self.permissions[group_id].max_monthly_urls if quota == -1: @@ -269,24 +340,31 @@ class UserState: current_month = datetime.now().month current_year = datetime.now().year - user_urls = self.db.query(models.Archive).filter( - models.Archive.author_id == self.email, - models.Archive.group_id == group_id, - func.extract('month', models.Archive.created_at) == current_month, - func.extract('year', models.Archive.created_at) == current_year - ).count() + user_urls = ( + self.db.query(models.Archive) + .filter( + models.Archive.author_id == self.email, + models.Archive.group_id == group_id, + func.extract("month", models.Archive.created_at) + == current_month, + func.extract("year", models.Archive.created_at) == current_year, + ) + .count() + ) return user_urls < quota def has_quota_max_monthly_mbs(self, group_id: str) -> bool: """ - checks if a user has reached their monthly MBs quota for a group, if global then group should be empty string + Checks if a user has reached their monthly MBs quota for a group, if + global then group should be empty string """ quota = 0 if not group_id: quota = self.max_monthly_mbs else: - if group_id not in self.permissions: return False + if group_id not in self.permissions: + return False quota = self.permissions[group_id].max_monthly_mbs if quota == -1: @@ -296,19 +374,34 @@ class UserState: current_year = datetime.now().year # find and sum all user bytes over this month - user_bytes = self.db.query(models.Archive).filter( - models.Archive.author_id == self.email, - models.Archive.group_id == group_id, - func.extract('month', models.Archive.created_at) == current_month, - func.extract('year', models.Archive.created_at) == current_year - ).with_entities(func.coalesce(func.sum( - func.coalesce( - func.cast( - func.json_extract(models.Archive.result, '$.metadata.total_bytes'), - sqlalchemy.Integer - ), 0 + user_bytes = ( + self.db.query(models.Archive) + .filter( + models.Archive.author_id == self.email, + models.Archive.group_id == group_id, + func.extract("month", models.Archive.created_at) + == current_month, + func.extract("year", models.Archive.created_at) == current_year, ) - ), 0).label('total')).scalar() + .with_entities( + func.coalesce( + func.sum( + func.coalesce( + func.cast( + func.json_extract( + models.Archive.result, + "$.metadata.total_bytes", + ), + sqlalchemy.Integer, + ), + 0, + ) + ), + 0, + ).label("total") + ) + .scalar() + ) # convert bytes to mb user_mbs = int(user_bytes / 1024 / 1024) @@ -316,7 +409,7 @@ class UserState: def can_manually_trigger(self, group_id: str) -> bool: """ - checks if a user is allowed to manually trigger a sheet + Checks if a user is allowed to manually trigger a sheet """ if group_id not in self.permissions: return False @@ -325,18 +418,21 @@ class UserState: def is_sheet_frequency_allowed(self, group_id: str, frequency: str) -> bool: """ - checks if a user is allowed to create a sheet with this frequency for this group + Checks if a user is allowed to create a sheet with this frequency for + this group """ if group_id not in self.permissions: return False return frequency in self.permissions[group_id].sheet_frequency - def priority_group(self, group_id: str) -> str: + def priority_group(self, group_id: str) -> dict: priority = "low" for group in self.user_groups: - if group.id != group_id: continue - if not group.permissions: continue + if group.id != group_id: + continue + if not group.permissions: + continue priority = group.permissions.get("priority", priority) break return convert_priority_to_queue_dict(priority) diff --git a/app/web/endpoints/default.py b/app/web/endpoints/default.py index cd23d13..ce3edd2 100644 --- a/app/web/endpoints/default.py +++ b/app/web/endpoints/default.py @@ -1,4 +1,4 @@ - +from http import HTTPStatus from typing import Dict from fastapi import APIRouter, Depends, HTTPException @@ -15,38 +15,50 @@ default_router = APIRouter() @default_router.get("/") -async def home(): - return JSONResponse({"version": VERSION, "breakingChanges": BREAKING_CHANGES}) +async def home() -> JSONResponse: + return JSONResponse( + {"version": VERSION, "breakingChanges": BREAKING_CHANGES} + ) @default_router.get("/health") -async def health(): +async def health() -> JSONResponse: return JSONResponse({"status": "ok"}) -@default_router.get("/user/active", summary="Check if the user is active and can use the tool.") +@default_router.get( + "/user/active", summary="Check if the user is active and can use the tool." +) async def active( user: UserState = Depends(get_user_state), ) -> ActiveUser: - return {"active": user.active} + return ActiveUser(active=user.active) -@default_router.get("/user/permissions", summary="Get the user's global 'all' permissions and the permissions for each group they belong to.") +@default_router.get( + "/user/permissions", + summary="Get the user's global 'all' permissions and the permissions for each group they belong to.", +) def get_user_permissions( user: UserState = Depends(get_user_state), ) -> Dict[str, GroupInfo]: return user.permissions -@default_router.get("/user/usage", summary="Get the user's monthly URLs/MBs usage along with the total active sheets, breakdown by group.") + +@default_router.get( + "/user/usage", + summary="Get the user's monthly URLs/MBs usage along with the total active sheets, breakdown by group.", +) def get_user_usage( user: UserState = Depends(get_user_state), ) -> UsageResponse: if not user.active: - raise HTTPException(status_code=403, detail="User is not active.") + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="User is not active." + ) return user.usage() - -@default_router.get('/favicon.ico', include_in_schema=False) +@default_router.get("/favicon.ico", include_in_schema=False) async def favicon() -> FileResponse: return FileResponse("app/web/static/favicon.ico") diff --git a/app/web/endpoints/interoperability.py b/app/web/endpoints/interoperability.py index 7892bde..86ea037 100644 --- a/app/web/endpoints/interoperability.py +++ b/app/web/endpoints/interoperability.py @@ -1,4 +1,5 @@ import json +from http import HTTPStatus import sqlalchemy from auto_archiver.core import Metadata @@ -16,26 +17,39 @@ from app.web.config import ALLOW_ANY_EMAIL from app.web.security import token_api_key_auth -interoperability_router = APIRouter(prefix="/interop", tags=["Interoperability endpoints."]) +interoperability_router = APIRouter( + prefix="/interop", tags=["Interoperability endpoints."] +) # ----- endpoint to submit data archived elsewhere -@interoperability_router.post("/submit-archive", status_code=201, summary="Submit a manual archive entry, for data that was archived elsewhere.") +@interoperability_router.post( + "/submit-archive", + status_code=HTTPStatus.CREATED, + summary="Submit a manual archive entry, for data that was archived elsewhere.", +) def submit_manual_archive( manual: schemas.SubmitManualArchive, auth=Depends(token_api_key_auth), - db: Session = Depends(get_db_dependency) + db: Session = Depends(get_db_dependency), ): try: result: Metadata = Metadata.from_json(manual.result) except json.JSONDecodeError as e: log_error(e) - raise HTTPException(status_code=422, detail="Invalid JSON in result field.") + raise HTTPException( + status_code=HTTPStatus.UNPROCESSABLE_ENTITY, + detail="Invalid JSON in result field.", + ) from e manual.author_id = manual.author_id or ALLOW_ANY_EMAIL manual.tags.add("manual") - store_until = business_logic.get_store_archive_until_or_never(db, manual.group_id) - logger.debug(f"[MANUAL ARCHIVE] {manual.author_id} {manual.url} {store_until}") + store_until = business_logic.get_store_archive_until_or_never( + db, manual.group_id + ) + logger.debug( + f"[MANUAL ARCHIVE] {manual.author_id} {manual.url} {store_until}" + ) try: archive = schemas.ArchiveCreate( @@ -51,8 +65,15 @@ def submit_manual_archive( ) db_archive = worker_crud.store_archived_url(db, archive) - logger.debug(f"[MANUAL ARCHIVE STORED] {db_archive.author_id} {db_archive.url}") - return JSONResponse({"id": db_archive.id}, status_code=201) + logger.debug( + f"[MANUAL ARCHIVE STORED] {db_archive.author_id} {db_archive.url}" + ) + return JSONResponse( + {"id": db_archive.id}, status_code=HTTPStatus.CREATED + ) except sqlalchemy.exc.IntegrityError as e: log_error(e) - raise HTTPException(status_code=422, detail=f"Cannot insert into DB due to integrity error, likely duplicate urls.") + raise HTTPException( + status_code=HTTPStatus.UNPROCESSABLE_ENTITY, + detail="Cannot insert into DB due to integrity error, likely duplicate urls.", + ) from e diff --git a/app/web/endpoints/sheet.py b/app/web/endpoints/sheet.py index d8c089a..5247107 100644 --- a/app/web/endpoints/sheet.py +++ b/app/web/endpoints/sheet.py @@ -1,81 +1,134 @@ +from http import HTTPStatus from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import JSONResponse from sqlalchemy import exc from sqlalchemy.orm import Session -from app.shared import schemas from app.shared.db.database import get_db_dependency +from app.shared.schemas import ( + DeleteResponse, + SheetAdd, + SheetResponse, + SubmitSheet, +) from app.shared.task_messaging import get_celery from app.web.db import crud from app.web.db.user_state import UserState from app.web.security import get_user_state -sheet_router = APIRouter(prefix="/sheet", tags=["Google Spreadsheet operations"]) +sheet_router = APIRouter( + prefix="/sheet", tags=["Google Spreadsheet operations"] +) celery = get_celery() -@sheet_router.post("/create", status_code=201, summary="Store a new Google Sheet for regular archiving.") + +@sheet_router.post( + "/create", + status_code=HTTPStatus.CREATED, + summary="Store a new Google Sheet for regular archiving.", +) def create_sheet( - sheet: schemas.SheetAdd, + sheet: SheetAdd, user: UserState = Depends(get_user_state), db: Session = Depends(get_db_dependency), -) -> schemas.SheetResponse: - +) -> SheetResponse: if not user.in_group(sheet.group_id): - raise HTTPException(status_code=403, detail="User does not have access to this group.") + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="User does not have access to this group.", + ) if not user.has_quota_monthly_sheets(sheet.group_id): - raise HTTPException(status_code=429, detail="User has reached their sheet quota for this group.") + raise HTTPException( + status_code=HTTPStatus.TOO_MANY_REQUESTS, + detail="User has reached their sheet quota for this group.", + ) if not user.is_sheet_frequency_allowed(sheet.group_id, sheet.frequency): - raise HTTPException(status_code=422, detail="Invalid frequency selected for this group.") + raise HTTPException( + status_code=HTTPStatus.UNPROCESSABLE_ENTITY, + detail="Invalid frequency selected for this group.", + ) try: - return crud.create_sheet(db, sheet.id, sheet.name, user.email, sheet.group_id, sheet.frequency) + return crud.create_sheet( + db, + sheet.id, + sheet.name, + user.email, + sheet.group_id, + sheet.frequency, + ) except exc.IntegrityError as e: - raise HTTPException(status_code=400, detail="Sheet with this ID is already being archived.") from e + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="Sheet with this ID is already being archived.", + ) from e -@sheet_router.get("/mine", status_code=200, summary="Get the authenticated user's Google Sheets.") +@sheet_router.get( + "/mine", + status_code=HTTPStatus.OK, + summary="Get the authenticated user's Google Sheets.", +) def get_user_sheets( user: UserState = Depends(get_user_state), - db: Session = Depends(get_db_dependency) -) -> list[schemas.SheetResponse]: + db: Session = Depends(get_db_dependency), +) -> list[SheetResponse]: return crud.get_user_sheets(db, user.email) -@sheet_router.delete("/{id}", summary="Delete a Google Sheet by ID.") +@sheet_router.delete("/{sheet_id}", summary="Delete a Google Sheet by ID.") def delete_sheet( - id: str, + sheet_id: str, user: UserState = Depends(get_user_state), db: Session = Depends(get_db_dependency), -) -> schemas.DeleteResponse: - return JSONResponse({ - "id": id, - "deleted": crud.delete_sheet(db, id, user.email) - }) +) -> DeleteResponse: + return DeleteResponse( + id=sheet_id, deleted=crud.delete_sheet(db, sheet_id, user.email) + ) -@sheet_router.post("/{id}/archive", status_code=201, summary="Trigger an archiving task for a GSheet you own.", response_description="task_id for the archiving task.") +@sheet_router.post( + "/{sheet_id}/archive", + status_code=HTTPStatus.CREATED, + summary="Trigger an archiving task for a GSheet you own.", + response_description="task_id for the archiving task.", +) def archive_user_sheet( - id: str, + sheet_id: str, user: UserState = Depends(get_user_state), db: Session = Depends(get_db_dependency), -) -> schemas.Task: - - sheet = crud.get_user_sheet(db, user.email, sheet_id=id) +) -> JSONResponse: + sheet = crud.get_user_sheet(db, user.email, sheet_id=sheet_id) if not sheet: - raise HTTPException(status_code=403, detail="No access to this sheet.") + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="No access to this sheet." + ) if not user.in_group(sheet.group_id): - raise HTTPException(status_code=403, detail="User does not have access to this group.") + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="User does not have access to this group.", + ) if not user.can_manually_trigger(sheet.group_id): - raise HTTPException(status_code=429, detail="User cannot manually trigger sheet archiving in this group.") + raise HTTPException( + status_code=HTTPStatus.TOO_MANY_REQUESTS, + detail="User cannot manually trigger sheet archiving in this group.", + ) group_queue = user.priority_group(sheet.group_id) - task = celery.signature("create_sheet_task", args=[schemas.SubmitSheet(sheet_id=id, author_id=user.email, group_id=sheet.group_id).model_dump_json()]).apply_async(**group_queue) + task = celery.signature( + "create_sheet_task", + args=[ + SubmitSheet( + sheet_id=sheet_id, author_id=user.email, group_id=sheet.group_id + ).model_dump_json() + ], + ).apply_async(**group_queue) - return JSONResponse({"id": task.id}, status_code=201) + return JSONResponse({"id": task.id}, status_code=HTTPStatus.CREATED) diff --git a/app/web/endpoints/task.py b/app/web/endpoints/task.py index 3f2ff94..e9da444 100644 --- a/app/web/endpoints/task.py +++ b/app/web/endpoints/task.py @@ -14,8 +14,14 @@ task_router = APIRouter(prefix="/task", tags=["Async task operations"]) celery = get_celery() -@task_router.get("/{task_id}", summary="Check the status of an async task by its id, works for URLs and Sheet tasks.") -def get_status(task_id, email=Depends(get_token_or_user_auth)) -> schemas.TaskResult: + +@task_router.get( + "/{task_id}", + summary="Check the status of an async task by its id, works for URLs and Sheet tasks.", +) +def get_status( + task_id, email=Depends(get_token_or_user_auth) +) -> schemas.TaskResult: task = AsyncResult(task_id, app=celery) try: if task.status == "FAILURE": @@ -24,17 +30,17 @@ def get_status(task_id, email=Depends(get_token_or_user_auth)) -> schemas.TaskRe # https://docs.celeryq.dev/en/stable/_modules/celery/result.html#AsyncResult raise task.result - response = { - "id": task_id, - "status": task.status, - "result": task.result - } - return JSONResponse(jsonable_encoder(response, exclude_unset=True, custom_encoder={bytes: custom_jsonable_encoder})) + response = {"id": task_id, "status": task.status, "result": task.result} + return JSONResponse( + jsonable_encoder( + response, + exclude_unset=True, + custom_encoder={bytes: custom_jsonable_encoder}, + ) + ) except Exception as e: log_error(e) - return JSONResponse({ - "id": task_id, - "status": "FAILURE", - "result": {"error": str(e)} - }) + return JSONResponse( + {"id": task_id, "status": "FAILURE", "result": {"error": str(e)}} + ) diff --git a/app/web/endpoints/url.py b/app/web/endpoints/url.py index 8307c2d..10b0adc 100644 --- a/app/web/endpoints/url.py +++ b/app/web/endpoints/url.py @@ -1,5 +1,5 @@ - from datetime import datetime +from http import HTTPStatus from urllib.parse import urlparse from fastapi import APIRouter, Depends, HTTPException @@ -9,6 +9,7 @@ from sqlalchemy.orm import Session from app.shared import schemas from app.shared.db.database import get_db_dependency +from app.shared.schemas import DeleteResponse from app.shared.task_messaging import get_celery from app.web.config import ALLOW_ANY_EMAIL from app.web.db import crud @@ -21,65 +22,106 @@ url_router = APIRouter(prefix="/url", tags=["Single URL operations"]) celery = get_celery() -@url_router.post("/archive", status_code=201, summary="Submit a single URL archive request, starts an archiving task.", response_description="task_id for the archiving task, will match the archive id.") + +@url_router.post( + "/archive", + status_code=HTTPStatus.CREATED, + summary="Submit a single URL archive request, starts an archiving task.", + response_description="task_id for the archiving task, will match the archive id.", +) def archive_url( archive: schemas.ArchiveTrigger, email=Depends(get_token_or_user_auth), - db: Session = Depends(get_db_dependency) -) -> schemas.Task: - logger.info(f"new {archive.public=} task for {email=} and {archive.group_id=}: {archive.url}") + db: Session = Depends(get_db_dependency), +) -> JSONResponse: + logger.info( + f"new {archive.public=} task for {email=} and {archive.group_id=}: {archive.url}" + ) parsed_url = urlparse(archive.url) if not all([parsed_url.scheme, parsed_url.netloc]): - raise HTTPException(status_code=400, detail="Invalid URL received.") + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Invalid URL received." + ) archive_create = schemas.ArchiveCreate(**archive.model_dump()) if email != ALLOW_ANY_EMAIL: archive_create.author_id = email user = UserState(db, email) if archive.group_id and not user.in_group(archive.group_id): - raise HTTPException(status_code=403, detail="User does not have access to this group.") + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="User does not have access to this group.", + ) if not user.has_quota_max_monthly_urls(archive.group_id): - raise HTTPException(status_code=429, detail="User has reached their monthly URL quota.") + raise HTTPException( + status_code=HTTPStatus.TOO_MANY_REQUESTS, + detail="User has reached their monthly URL quota.", + ) if not user.has_quota_max_monthly_mbs(archive.group_id): - raise HTTPException(status_code=429, detail="User has reached their monthly MB quota.") + raise HTTPException( + status_code=HTTPStatus.TOO_MANY_REQUESTS, + detail="User has reached their monthly MB quota.", + ) group_queue = user.priority_group(archive_create.group_id) else: archive_create.author_id = archive.author_id or email group_queue = convert_priority_to_queue_dict("high") - - task = celery.signature("create_archive_task", args=[archive_create.model_dump_json()]).apply_async(**group_queue) + task = celery.signature( + "create_archive_task", args=[archive_create.model_dump_json()] + ).apply_async(**group_queue) task_response = schemas.Task(id=task.id) - return JSONResponse(task_response.model_dump(), status_code=201) + return JSONResponse( + task_response.model_dump(), status_code=HTTPStatus.CREATED + ) @url_router.get("/search", summary="Search for archive entries by URL.") def search_by_url( - url: str, skip: int = 0, limit: int = 25, - archived_after: datetime = None, archived_before: datetime = None, - db: Session = Depends(get_db_dependency), - email: str = Depends(get_token_or_user_auth) + url: str, + skip: int = 0, + limit: int = 25, + archived_after: datetime = None, + archived_before: datetime = None, + db: Session = Depends(get_db_dependency), + email: str = Depends(get_token_or_user_auth), ) -> list[schemas.ArchiveResult]: - read_groups, read_public = False, False if email != ALLOW_ANY_EMAIL: user = UserState(db, email) if not user.read and not user.read_public: - raise HTTPException(status_code=403, detail="User does not have read access.") + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, + detail="User does not have read access.", + ) read_groups = user.read read_public = user.read_public - return crud.search_archives_by_url(db, url.strip(), email, read_groups, read_public, skip=skip, limit=limit, archived_after=archived_after, archived_before=archived_before) + return crud.search_archives_by_url( + db, + url.strip(), + email, + read_groups, + read_public, + skip=skip, + limit=limit, + archived_after=archived_after, + archived_before=archived_before, + ) -@url_router.delete("/{id}", summary="Delete a single URL archive by id.") +@url_router.delete( + "/{archive_id}", summary="Delete a single URL archive by id." +) def delete_archive( - id:str, + archive_id: str, user: UserState = Depends(get_user_state), - db: Session = Depends(get_db_dependency) -) -> schemas.DeleteResponse: - logger.info(f"deleting url archive task {id} request by {user.email}") - return JSONResponse({ - "id": id, - "deleted": crud.soft_delete_archive(db, id, user.email) - }) + db: Session = Depends(get_db_dependency), +) -> DeleteResponse: + logger.info( + f"deleting url archive task {archive_id} request by {user.email}" + ) + return DeleteResponse( + id=archive_id, + deleted=crud.soft_delete_archive(db, archive_id, user.email), + ) diff --git a/app/web/main.py b/app/web/main.py index 69af5c6..525eff4 100644 --- a/app/web/main.py +++ b/app/web/main.py @@ -6,7 +6,7 @@ from fastapi.staticfiles import StaticFiles from loguru import logger from prometheus_fastapi_instrumentator import Instrumentator -from app.shared.settings import get_settings +from app.shared.settings import Settings, get_settings from app.shared.task_messaging import get_celery from app.web.config import API_DESCRIPTION, VERSION from app.web.endpoints.default import default_router @@ -21,13 +21,22 @@ from app.web.security import token_api_key_auth celery = get_celery() -def app_factory(settings = get_settings()): + +def app_factory(settings: Settings = None): + # TODO: Create dev, test, and prod versions of settings that do not have + # TODO: to be passed in as a parameter + if settings is None: + settings = get_settings() + app = FastAPI( title="Auto-Archiver API", description=API_DESCRIPTION, version=VERSION, - contact={"name": "GitHub", "url": "https://github.com/bellingcat/auto-archiver-api"}, - lifespan=lifespan + contact={ + "name": "GitHub", + "url": "https://github.com/bellingcat/auto-archiver-api", + }, + lifespan=lifespan, ) app.add_middleware( @@ -46,14 +55,30 @@ def app_factory(settings = get_settings()): app.include_router(interoperability_router) # prometheus exposed in /metrics with authentication - Instrumentator(should_group_status_codes=False, excluded_handlers=["/metrics", "/health", "/openapi.json", "/favicon.ico"]).instrument(app).expose(app, dependencies=[Depends(token_api_key_auth)]) + Instrumentator( + should_group_status_codes=False, + excluded_handlers=[ + "/metrics", + "/health", + "/openapi.json", + "/favicon.ico", + ], + ).instrument(app).expose(app, dependencies=[Depends(token_api_key_auth)]) if settings.SERVE_LOCAL_ARCHIVE: local_dir = settings.SERVE_LOCAL_ARCHIVE - if not os.path.isdir(local_dir) and os.path.isdir(local_dir.replace("/app", ".")): + if not os.path.isdir(local_dir) and os.path.isdir( + local_dir.replace("/app", ".") + ): local_dir = local_dir.replace("/app", ".") if len(settings.SERVE_LOCAL_ARCHIVE) > 1 and os.path.isdir(local_dir): - logger.warning(f"MOUNTing local archive, use this in development only {settings.SERVE_LOCAL_ARCHIVE}") - app.mount(settings.SERVE_LOCAL_ARCHIVE, StaticFiles(directory=local_dir), name=settings.SERVE_LOCAL_ARCHIVE) + logger.warning( + f"MOUNTing local archive, use this in development only {settings.SERVE_LOCAL_ARCHIVE}" + ) + app.mount( + settings.SERVE_LOCAL_ARCHIVE, + StaticFiles(directory=local_dir), + name=settings.SERVE_LOCAL_ARCHIVE, + ) return app diff --git a/app/web/middleware.py b/app/web/middleware.py index 5ddca4b..47c07b3 100644 --- a/app/web/middleware.py +++ b/app/web/middleware.py @@ -1,4 +1,3 @@ - import traceback from fastapi import Request @@ -11,23 +10,30 @@ from app.web.utils.metrics import EXCEPTION_COUNTER async def logging_middleware(request: Request, call_next): try: response = await call_next(request) - #TODO: use Origin to have summary prometheus metrics on where requests come from + # TODO: use Origin to have summary prometheus metrics on where requests come from # origin = request.headers.get("origin") - logger.info(f"{request.client.host}:{request.client.port} {request.method} {request.url._url} - HTTP {response.status_code}") + logger.info( + f"{request.client.host}:{request.client.port} {request.method} {request.url._url} - HTTP {response.status_code}" + ) return response except Exception as e: location = f"{request.method} {request.url._url}" await increase_exceptions_counter(e, location) - logger.info(f"{request.client.host}:{request.client.port} {location} - {e.__class__.__name__} {e}") + logger.info( + f"{request.client.host}:{request.client.port} {location} - {e.__class__.__name__} {e}" + ) raise e -async def increase_exceptions_counter(e: Exception, location:str="cronjob"): + +async def increase_exceptions_counter(e: Exception, location: str = "cronjob"): if location == "cronjob": try: last_trace = traceback.extract_tb(e.__traceback__)[-1] _file, _line, func_name, _text = last_trace location = func_name except Exception as e: - logger.error(f"Unable to get function name from cronjob exception traceback: {e}") + logger.error( + f"Unable to get function name from cronjob exception traceback: {e}" + ) EXCEPTION_COUNTER.labels(type=e.__class__.__name__, location=location).inc() log_error(e) diff --git a/app/web/security.py b/app/web/security.py index 494e094..5850ad3 100644 --- a/app/web/security.py +++ b/app/web/security.py @@ -1,4 +1,5 @@ import secrets +from http import HTTPStatus import requests from fastapi import Depends, HTTPException, status @@ -16,7 +17,7 @@ settings = get_settings() bearer_security = HTTPBearer() -def secure_compare(token, api_key): +def secure_compare(token, api_key) -> bool: return secrets.compare_digest(token.encode("utf8"), api_key.encode("utf8")) @@ -24,9 +25,13 @@ def secure_compare(token, api_key): def api_key_auth(api_key): assert len(api_key) >= 20, "Invalid API key, must be at least 20 chars" - async def auth(bearer: HTTPAuthorizationCredentials = Depends(bearer_security), auto_error=True): + async def auth( + bearer: HTTPAuthorizationCredentials = Depends(bearer_security), + auto_error=True, + ): is_correct = secure_compare(bearer.credentials, api_key) - if is_correct: return True + if is_correct: + return True if auto_error: raise HTTPException( @@ -38,17 +43,22 @@ def api_key_auth(api_key): return auth -# --------------------- Token Auth for AA itself to query the API, AA setup tool and Prometheus +# --- Token Auth for AA itself to query the API, AA setup tool and Prometheus token_api_key_auth = api_key_auth(settings.API_BEARER_TOKEN) -async def get_token_or_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bearer_security)): +async def get_token_or_user_auth( + credentials: HTTPAuthorizationCredentials = Depends(bearer_security), +): # tries to use the static API_KEY and defaults to google JWT auth - if await token_api_key_auth(credentials, auto_error=False): return ALLOW_ANY_EMAIL + if await token_api_key_auth(credentials, auto_error=False): + return ALLOW_ANY_EMAIL return await get_user_auth(credentials) -async def get_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bearer_security)): +async def get_user_auth( + credentials: HTTPAuthorizationCredentials = Depends(bearer_security), +): # validates the Bearer token in the case that it requires it valid_user, info = authenticate_user(credentials.credentials) if valid_user: @@ -61,26 +71,37 @@ async def get_user_auth(credentials: HTTPAuthorizationCredentials = Depends(bear ) -def authenticate_user(access_token): +def authenticate_user(access_token) -> (bool, str): # https://cloud.google.com/docs/authentication/token-types#access - if type(access_token) != str or len(access_token) < 10: return False, "invalid access_token" - r = requests.get("https://oauth2.googleapis.com/tokeninfo", {"access_token": access_token}) - if r.status_code != 200: return False, "invalid token" + if not isinstance(access_token, str) or len(access_token) < 10: + return False, "invalid access_token" + r = requests.get( + "https://oauth2.googleapis.com/tokeninfo", + {"access_token": access_token}, + ) + if r.status_code != HTTPStatus.OK: + return False, "invalid token" try: j = r.json() - if j.get("azp") not in settings.CHROME_APP_IDS and j.get("aud") not in settings.CHROME_APP_IDS: - return False, f"token does not belong to valid APP_ID" + if ( + j.get("azp") not in settings.CHROME_APP_IDS + and j.get("aud") not in settings.CHROME_APP_IDS + ): + return False, "token does not belong to valid APP_ID" if j.get("email") in settings.BLOCKED_EMAILS: return False, f"email '{j.get('email')}' not allowed" if j.get("email_verified") != "true": return False, f"email '{j.get('email')}' not verified" if int(j.get("expires_in", -1)) <= 0: return False, "Token expired" - return True, j.get('email').lower() + return True, j.get("email").lower() except Exception as e: logger.warning(f"AUTH EXCEPTION occurred: {e}") return False, "exception occurred" -def get_user_state(email:str=Depends(get_user_auth), db:Session=Depends(get_db_dependency)): +def get_user_state( + email: str = Depends(get_user_auth), + db: Session = Depends(get_db_dependency), +) -> UserState: return UserState(db, email) diff --git a/app/web/utils/metrics.py b/app/web/utils/metrics.py index d8026a1..04b496f 100644 --- a/app/web/utils/metrics.py +++ b/app/web/utils/metrics.py @@ -15,27 +15,25 @@ from app.web.db import crud EXCEPTION_COUNTER = Counter( "exceptions", "Number of times a certain exception has occurred.", - labelnames=["type", "location"] + labelnames=["type", "location"], ) WORKER_EXCEPTION = Counter( "worker_exceptions_total", "Number of times a certain exception has occurred on the worker.", - labelnames=["type", "exception", "task", "traceback"] + labelnames=["type", "exception", "task", "traceback"], ) DISK_UTILIZATION = Gauge( - "disk_utilization", - "Disk utilization in GB", - labelnames=["type"] + "disk_utilization", "Disk utilization in GB", labelnames=["type"] ) DATABASE_METRICS = Gauge( "database_metrics", "Database metric readings at a certain point in time", - labelnames=["query"] + labelnames=["query"], ) DATABASE_METRICS_COUNTER = Counter( "database_metrics_counter", "Database metrics that increase over time", - labelnames=["query", "user"] + labelnames=["query", "user"], ) @@ -48,7 +46,12 @@ async def redis_subscribe_worker_exceptions(REDIS_EXCEPTIONS_CHANNEL: str): message = PubSubExceptions.get_message() if message and message["type"] == "message": data = json.loads(message["data"].decode("utf-8")) - WORKER_EXCEPTION.labels(type=data["type"], exception=data["exception"], task=data["task"], traceback=data["traceback"]).inc() + WORKER_EXCEPTION.labels( + type=data["type"], + exception=data["exception"], + task=data["task"], + traceback=data["traceback"], + ).inc() await asyncio.sleep(1) @@ -59,12 +62,19 @@ async def measure_regular_metrics(sqlite_db_url: str, repeat_in_seconds: int): try: fs = os.stat(sqlite_db_url.replace("sqlite:///", "")) DISK_UTILIZATION.labels(type="database").set(fs.st_size / (2**30)) - except Exception as e: log_error(e) + except Exception as e: + log_error(e) with get_db() as db: - DATABASE_METRICS.labels(query="count_archives").set(crud.count_archives(db)) - DATABASE_METRICS.labels(query="count_archive_urls").set(crud.count_archive_urls(db)) + DATABASE_METRICS.labels(query="count_archives").set( + crud.count_archives(db) + ) + DATABASE_METRICS.labels(query="count_archive_urls").set( + crud.count_archive_urls(db) + ) DATABASE_METRICS.labels(query="count_users").set(crud.count_users(db)) for user in crud.count_by_user_since(db, repeat_in_seconds): - DATABASE_METRICS_COUNTER.labels(query="count_by_user", user=user.author_id).inc(user.total) + DATABASE_METRICS_COUNTER.labels( + query="count_by_user", user=user.author_id + ).inc(user.total) diff --git a/app/web/utils/misc.py b/app/web/utils/misc.py index 16a6591..f78ae1e 100644 --- a/app/web/utils/misc.py +++ b/app/web/utils/misc.py @@ -5,12 +5,12 @@ from fastapi.encoders import jsonable_encoder def custom_jsonable_encoder(obj): if isinstance(obj, bytes): - return base64.b64encode(obj).decode('utf-8') + return base64.b64encode(obj).decode("utf-8") return jsonable_encoder(obj) def convert_priority_to_queue_dict(priority: str) -> dict: return { "priority": 0 if priority == "high" else 10, - "queue": f"{priority}_priority" + "queue": f"{priority}_priority", } diff --git a/pyproject.toml b/pyproject.toml index 75f6e63..1a849ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,9 @@ pythonpath = "." [tool.coverage.run] omit = ["app/migrations/*"] +[tool.ruff.lint.flake8-bugbear] +extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] + [tool.poetry.group.worker.dependencies] watchdog = ">=6.0.0,<7.0.0" setuptools = "^75.8.0" From 53ac51a08cb8763158b483a32e601d630dfd4b60 Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:47:06 -0500 Subject: [PATCH 16/40] Delete aa_utils.py --- app/shared/aa_utils.py | 49 ------------------------------------------ 1 file changed, 49 deletions(-) delete mode 100644 app/shared/aa_utils.py diff --git a/app/shared/aa_utils.py b/app/shared/aa_utils.py deleted file mode 100644 index 1021f8e..0000000 --- a/app/shared/aa_utils.py +++ /dev/null @@ -1,49 +0,0 @@ -# TODO: code in this file should eventually be moved to the auto-archiver code base - -from typing import List - -from auto_archiver.core import Media, Metadata -from loguru import logger - -from app.shared.db import models - - -def get_all_urls(result: Metadata) -> List[models.ArchiveUrl]: - db_urls = [] - for m in result.media: - for i, url in enumerate(m.urls): - db_urls.append( - models.ArchiveUrl(url=url, key=m.get("id", f"media_{i}")) - ) - for k, prop in m.properties.items(): - if prop_converted := convert_if_media(prop): - for i, url in enumerate(prop_converted.urls): - db_urls.append( - models.ArchiveUrl( - url=url, key=prop_converted.get("id", f"{k}_{i}") - ) - ) - if isinstance(prop, list): - for i, prop_media in enumerate(prop): - if prop_media := convert_if_media(prop_media): - for j, url in enumerate(prop_media.urls): - db_urls.append( - models.ArchiveUrl( - url=url, - key=prop_media.get( - "id", f"{k}{prop_media.key}_{i}.{j}" - ), - ) - ) - return db_urls - - -def convert_if_media(media): - if isinstance(media, Media): - return media - elif isinstance(media, dict): - try: - return Media.from_dict(media) - except Exception as e: - logger.debug(f"error parsing {media} : {e}") - return False From 297f8810beab270bb53dd331ac099d10b2ef7297 Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:47:15 -0500 Subject: [PATCH 17/40] Update misc.py --- app/web/utils/misc.py | 46 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/app/web/utils/misc.py b/app/web/utils/misc.py index f78ae1e..b561975 100644 --- a/app/web/utils/misc.py +++ b/app/web/utils/misc.py @@ -1,6 +1,11 @@ import base64 +from typing import List +from auto_archiver.core import Media, Metadata from fastapi.encoders import jsonable_encoder +from loguru import logger + +from app.shared.db import models def custom_jsonable_encoder(obj): @@ -14,3 +19,44 @@ def convert_priority_to_queue_dict(priority: str) -> dict: "priority": 0 if priority == "high" else 10, "queue": f"{priority}_priority", } + + +def convert_if_media(media): + if isinstance(media, Media): + return media + elif isinstance(media, dict): + try: + return Media.from_dict(media) + except Exception as e: + logger.debug(f"error parsing {media} : {e}") + return False + + +def get_all_urls(result: Metadata) -> List[models.ArchiveUrl]: + db_urls = [] + for m in result.media: + for i, url in enumerate(m.urls): + db_urls.append( + models.ArchiveUrl(url=url, key=m.get("id", f"media_{i}")) + ) + for k, prop in m.properties.items(): + if prop_converted := convert_if_media(prop): + for i, url in enumerate(prop_converted.urls): + db_urls.append( + models.ArchiveUrl( + url=url, key=prop_converted.get("id", f"{k}_{i}") + ) + ) + if isinstance(prop, list): + for i, prop_media in enumerate(prop): + if prop_media := convert_if_media(prop_media): + for j, url in enumerate(prop_media.urls): + db_urls.append( + models.ArchiveUrl( + url=url, + key=prop_media.get( + "id", f"{k}{prop_media.key}_{i}.{j}" + ), + ) + ) + return db_urls From 0e2122b72fadfcaae70455f1f015bf5a1514e80a Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:52:54 -0500 Subject: [PATCH 18/40] oi --- app/tests/worker/test_worker_main.py | 3 ++- app/web/endpoints/interoperability.py | 2 +- app/worker/main.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py index e67aa88..865d039 100644 --- a/app/tests/worker/test_worker_main.py +++ b/app/tests/worker/test_worker_main.py @@ -6,7 +6,8 @@ from auto_archiver.core import Media, Metadata from app.shared import schemas from app.shared.db import models -from app.worker.main import create_archive_task, create_sheet_task, get_all_urls +from app.web.utils.misc import get_all_urls +from app.worker.main import create_archive_task, create_sheet_task class TestCreateArchiveTask: diff --git a/app/web/endpoints/interoperability.py b/app/web/endpoints/interoperability.py index 86ea037..d08bf50 100644 --- a/app/web/endpoints/interoperability.py +++ b/app/web/endpoints/interoperability.py @@ -9,12 +9,12 @@ from loguru import logger from sqlalchemy.orm import Session from app.shared import business_logic, schemas -from app.shared.aa_utils import get_all_urls from app.shared.db import models, worker_crud from app.shared.db.database import get_db_dependency from app.shared.log import log_error from app.web.config import ALLOW_ANY_EMAIL from app.web.security import token_api_key_auth +from app.web.utils.misc import get_all_urls interoperability_router = APIRouter( diff --git a/app/worker/main.py b/app/worker/main.py index f7b2915..7716b6b 100644 --- a/app/worker/main.py +++ b/app/worker/main.py @@ -8,12 +8,12 @@ from loguru import logger from sqlalchemy import exc from app.shared import business_logic, schemas -from app.shared.aa_utils import get_all_urls from app.shared.db import models, worker_crud from app.shared.db.database import get_db from app.shared.log import log_error from app.shared.settings import get_settings from app.shared.task_messaging import get_celery, get_redis +from app.web.utils.misc import get_all_urls from app.worker.worker_log import setup_celery_logger From a59a916b7e4b6b269249d2893dc0e8e09c8d00e1 Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:57:10 -0500 Subject: [PATCH 19/40] Update metrics.py --- app/web/utils/metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/web/utils/metrics.py b/app/web/utils/metrics.py index 04b496f..5358cc5 100644 --- a/app/web/utils/metrics.py +++ b/app/web/utils/metrics.py @@ -37,11 +37,11 @@ DATABASE_METRICS_COUNTER = Counter( ) -async def redis_subscribe_worker_exceptions(REDIS_EXCEPTIONS_CHANNEL: str): +async def redis_subscribe_worker_exceptions(redis_exceptions_channel: str): # Subscribe to Redis channel and increment the counter for each exception with info on the exception and task Redis = get_redis() PubSubExceptions = Redis.pubsub() - PubSubExceptions.subscribe(REDIS_EXCEPTIONS_CHANNEL) + PubSubExceptions.subscribe(redis_exceptions_channel) while True: message = PubSubExceptions.get_message() if message and message["type"] == "message": From a7a5b16ae674067dfb3c80dad12c97265f65538d Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Fri, 14 Mar 2025 22:45:20 -0500 Subject: [PATCH 20/40] Update metrics.py --- app/web/utils/metrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/web/utils/metrics.py b/app/web/utils/metrics.py index 5358cc5..55f17e8 100644 --- a/app/web/utils/metrics.py +++ b/app/web/utils/metrics.py @@ -38,7 +38,8 @@ DATABASE_METRICS_COUNTER = Counter( async def redis_subscribe_worker_exceptions(redis_exceptions_channel: str): - # Subscribe to Redis channel and increment the counter for each exception with info on the exception and task + # Subscribe to Redis channel and increment the counter for each exception + # with info on the exception and task Redis = get_redis() PubSubExceptions = Redis.pubsub() PubSubExceptions.subscribe(redis_exceptions_channel) From 05e3755d65682f4ba092c84820c3eff993ac3442 Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Fri, 14 Mar 2025 22:48:36 -0500 Subject: [PATCH 21/40] boop --- app/web/endpoints/task.py | 3 ++- app/web/middleware.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/web/endpoints/task.py b/app/web/endpoints/task.py index e9da444..3581ab2 100644 --- a/app/web/endpoints/task.py +++ b/app/web/endpoints/task.py @@ -26,7 +26,8 @@ def get_status( try: if task.status == "FAILURE": # *FAILURE* The task raised an exception, or has exceeded the retry limit. - # The :attr:`result` attribute then contains the exception raised by the task. + # The :attr:`result` attribute then contains the exception raised by + # the task. # https://docs.celeryq.dev/en/stable/_modules/celery/result.html#AsyncResult raise task.result diff --git a/app/web/middleware.py b/app/web/middleware.py index 47c07b3..b39ae01 100644 --- a/app/web/middleware.py +++ b/app/web/middleware.py @@ -10,7 +10,8 @@ from app.web.utils.metrics import EXCEPTION_COUNTER async def logging_middleware(request: Request, call_next): try: response = await call_next(request) - # TODO: use Origin to have summary prometheus metrics on where requests come from + # TODO: use Origin to have summary prometheus metrics on where + # requests come from # origin = request.headers.get("origin") logger.info( f"{request.client.host}:{request.client.port} {request.method} {request.url._url} - HTTP {response.status_code}" @@ -25,7 +26,9 @@ async def logging_middleware(request: Request, call_next): raise e -async def increase_exceptions_counter(e: Exception, location: str = "cronjob"): +async def increase_exceptions_counter( + e: Exception, location: str = "cronjob" +) -> None: if location == "cronjob": try: last_trace = traceback.extract_tb(e.__traceback__)[-1] From ba7ed5727cdf2e6c880bd8b09520c37acb23d4b2 Mon Sep 17 00:00:00 2001 From: michplunkett <5885605+michplunkett@users.noreply.github.com> Date: Fri, 14 Mar 2025 22:49:26 -0500 Subject: [PATCH 22/40] Update security.py --- app/web/security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/web/security.py b/app/web/security.py index 5850ad3..ecb0111 100644 --- a/app/web/security.py +++ b/app/web/security.py @@ -59,7 +59,7 @@ async def get_token_or_user_auth( async def get_user_auth( credentials: HTTPAuthorizationCredentials = Depends(bearer_security), ): - # validates the Bearer token in the case that it requires it + # Validates the Bearer token in the case that it requires it valid_user, info = authenticate_user(credentials.credentials) if valid_user: return info.lower() From 7012049098b48933112c7dcd54e1ed5dbd7331c9 Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Mon, 17 Mar 2025 17:52:13 +0000 Subject: [PATCH 23/40] adds pre-commit instructions to the readme --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index da402e3..079a747 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,24 @@ Make sure environment and user-groups files are up to date. Then `make prod`. +## Development +```bash +# make sure all development dependencies are installed +poetry install --with dev + +# this project uses pre-commit to enforce code style and formatting, set that up locally +poetry run pre-commit install + +# you can test pre-commit with +poetry run pre-commit run --all-files + +# this means pre-commit will always run with git commit, to skip it use +git commit --no-verify + +# see the Makefile for more commands, but linting and formatting can be done with +make lint +``` + ### Testing ```bash # set the testing environment variables From 1a3546c09e45833d7e293ce44d64a17904fde85e Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Mon, 17 Mar 2025 18:06:42 +0000 Subject: [PATCH 24/40] adds test command to makefile and readme --- Makefile | 6 ++++++ README.md | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Makefile b/Makefile index ddda086..05a4bff 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,12 @@ lint: poetry run pre-commit run --all-files +.PHONY: test +test: + export ENVIRONMENT_FILE=.env.test + poetry run coverage run -m pytest -v --disable-warnings --color=yes app/tests/ + poetry run coverage report + .PHONY: clean-dev clean-dev: @echo -n "Are you sure? [yes/N] (this will delete volumes) " && read ans && [ $${ans:-N} = yes ] diff --git a/README.md b/README.md index 079a747..16ed669 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,9 @@ git commit --no-verify # see the Makefile for more commands, but linting and formatting can be done with make lint + +# run all tests +make test ``` ### Testing From de6800ea549833898e37169fd1a0f0982a26a829 Mon Sep 17 00:00:00 2001 From: Michael Plunkett <5885605+michplunkett@users.noreply.github.com> Date: Wed, 2 Apr 2025 13:14:09 -0500 Subject: [PATCH 25/40] Standardize router names (#70) --- app/shared/constants.py | 4 ++++ .../{endpoints => routers}/test_default.py | 0 .../test_interoperability.py | 4 ++-- .../web/{endpoints => routers}/test_sheet.py | 5 +++-- .../web/{endpoints => routers}/test_task.py | 20 ++++++++++--------- .../web/{endpoints => routers}/test_url.py | 15 +++++++------- app/tests/web/test_main.py | 2 +- app/web/main.py | 10 +++++----- app/web/{endpoints => routers}/__init__.py | 0 app/web/{endpoints => routers}/default.py | 14 ++++++------- .../interoperability.py | 6 ++---- app/web/{endpoints => routers}/sheet.py | 12 +++++------ app/web/{endpoints => routers}/task.py | 13 ++++++++---- app/web/{endpoints => routers}/url.py | 10 ++++------ 14 files changed, 61 insertions(+), 54 deletions(-) create mode 100644 app/shared/constants.py rename app/tests/web/{endpoints => routers}/test_default.py (100%) rename app/tests/web/{endpoints => routers}/test_interoperability.py (96%) rename app/tests/web/{endpoints => routers}/test_sheet.py (98%) rename app/tests/web/{endpoints => routers}/test_task.py (71%) rename app/tests/web/{endpoints => routers}/test_url.py (96%) rename app/web/{endpoints => routers}/__init__.py (100%) rename app/web/{endpoints => routers}/default.py (88%) rename app/web/{endpoints => routers}/interoperability.py (94%) rename app/web/{endpoints => routers}/sheet.py (94%) rename app/web/{endpoints => routers}/task.py (81%) rename app/web/{endpoints => routers}/url.py (94%) diff --git a/app/shared/constants.py b/app/shared/constants.py new file mode 100644 index 0000000..90a5067 --- /dev/null +++ b/app/shared/constants.py @@ -0,0 +1,4 @@ +# Statuses +STATUS_FAILURE = "FAILURE" +STATUS_PENDING = "PENDING" +STATUS_SUCCESS = "SUCCESS" diff --git a/app/tests/web/endpoints/test_default.py b/app/tests/web/routers/test_default.py similarity index 100% rename from app/tests/web/endpoints/test_default.py rename to app/tests/web/routers/test_default.py diff --git a/app/tests/web/endpoints/test_interoperability.py b/app/tests/web/routers/test_interoperability.py similarity index 96% rename from app/tests/web/endpoints/test_interoperability.py rename to app/tests/web/routers/test_interoperability.py index d102116..d53c289 100644 --- a/app/tests/web/endpoints/test_interoperability.py +++ b/app/tests/web/routers/test_interoperability.py @@ -15,7 +15,7 @@ def test_submit_manual_archive_not_user_auth(client_with_auth, test_no_auth): @patch( - "app.web.endpoints.interoperability.business_logic", + "app.web.routers.interoperability.business_logic", return_value=MagicMock( get_store_archive_until=MagicMock(return_value=datetime) ), @@ -103,7 +103,7 @@ def test_submit_manual_archive_invalid_json(client_with_token): @patch( - "app.web.endpoints.interoperability.business_logic.get_store_archive_until", + "app.web.routers.interoperability.business_logic.get_store_archive_until", side_effect=AssertionError("AssertionError"), ) def test_submit_manual_archive_no_store_until( diff --git a/app/tests/web/endpoints/test_sheet.py b/app/tests/web/routers/test_sheet.py similarity index 98% rename from app/tests/web/endpoints/test_sheet.py rename to app/tests/web/routers/test_sheet.py index c318496..41c6fd6 100644 --- a/app/tests/web/endpoints/test_sheet.py +++ b/app/tests/web/routers/test_sheet.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, patch from fastapi.testclient import TestClient +from app.shared.constants import STATUS_PENDING from app.shared.db import models from app.shared.schemas import TaskResult from app.web.db.user_state import UserState @@ -184,7 +185,7 @@ def test_delete_sheet_endpoint(client_with_auth, db_session): class TestArchiveUserSheetEndpoint: - @patch("app.web.endpoints.sheet.celery", return_value=MagicMock()) + @patch("app.web.routers.sheet.celery", return_value=MagicMock()) def test_normal_flow(self, m_celery, client_with_auth, db_session): db_session.add( models.Sheet( @@ -199,7 +200,7 @@ class TestArchiveUserSheetEndpoint: m_signature = MagicMock() m_signature.apply_async.return_value = TaskResult( - id="123-taskid", status="PENDING", result="" + id="123-taskid", status=STATUS_PENDING, result="" ) m_celery.signature.return_value = m_signature diff --git a/app/tests/web/endpoints/test_task.py b/app/tests/web/routers/test_task.py similarity index 71% rename from app/tests/web/endpoints/test_task.py rename to app/tests/web/routers/test_task.py index 038babf..8165d58 100644 --- a/app/tests/web/endpoints/test_task.py +++ b/app/tests/web/routers/test_task.py @@ -1,14 +1,16 @@ from http import HTTPStatus from unittest.mock import patch +from app.shared.constants import STATUS_FAILURE, STATUS_PENDING, STATUS_SUCCESS + def test_endpoint_task_status_no_auth(client, test_no_auth): test_no_auth(client.get, "/task/test-task-id") -@patch("app.web.endpoints.task.AsyncResult") +@patch("app.web.routers.task.AsyncResult") def test_get_status_success(mock_async_result, client_with_auth): - mock_async_result.return_value.status = "SUCCESS" + mock_async_result.return_value.status = STATUS_SUCCESS mock_async_result.return_value.result = {"data": "some result"} response = client_with_auth.get("/task/test-task-id") @@ -16,14 +18,14 @@ def test_get_status_success(mock_async_result, client_with_auth): assert response.status_code == HTTPStatus.OK assert response.json() == { "id": "test-task-id", - "status": "SUCCESS", + "status": STATUS_SUCCESS, "result": {"data": "some result"}, } -@patch("app.web.endpoints.task.AsyncResult") +@patch("app.web.routers.task.AsyncResult") def test_get_status_failure(mock_async_result, client_with_auth): - mock_async_result.return_value.status = "FAILURE" + mock_async_result.return_value.status = STATUS_FAILURE mock_async_result.return_value.result = Exception("Some error") response = client_with_auth.get("/task/test-task-id") @@ -31,14 +33,14 @@ def test_get_status_failure(mock_async_result, client_with_auth): assert response.status_code == HTTPStatus.OK assert response.json() == { "id": "test-task-id", - "status": "FAILURE", + "status": STATUS_FAILURE, "result": {"error": "Some error"}, } -@patch("app.web.endpoints.task.AsyncResult") +@patch("app.web.routers.task.AsyncResult") def test_get_status_pending(mock_async_result, client_with_auth): - mock_async_result.return_value.status = "PENDING" + mock_async_result.return_value.status = STATUS_PENDING mock_async_result.return_value.result = None response = client_with_auth.get("/task/test-task-id") @@ -46,6 +48,6 @@ def test_get_status_pending(mock_async_result, client_with_auth): assert response.status_code == HTTPStatus.OK assert response.json() == { "id": "test-task-id", - "status": "PENDING", + "status": STATUS_PENDING, "result": None, } diff --git a/app/tests/web/endpoints/test_url.py b/app/tests/web/routers/test_url.py similarity index 96% rename from app/tests/web/endpoints/test_url.py rename to app/tests/web/routers/test_url.py index cd64262..0d1d452 100644 --- a/app/tests/web/endpoints/test_url.py +++ b/app/tests/web/routers/test_url.py @@ -3,6 +3,7 @@ from http import HTTPStatus from unittest.mock import MagicMock, patch from app.shared import schemas +from app.shared.constants import STATUS_PENDING from app.shared.db import worker_crud from app.shared.schemas import ArchiveCreate, TaskResult from app.web.config import ALLOW_ANY_EMAIL @@ -12,12 +13,12 @@ def test_archive_url_unauthenticated(client, test_no_auth): test_no_auth(client.post, "/url/archive") -@patch("app.web.endpoints.url.UserState") -@patch("app.web.endpoints.url.celery", return_value=MagicMock()) +@patch("app.web.routers.url.UserState") +@patch("app.web.routers.url.celery", return_value=MagicMock()) def test_archive_url(m_celery, m2, client_with_auth): m_signature = MagicMock() m_signature.apply_async.return_value = TaskResult( - id="123-456-789", status="PENDING", result="" + id="123-456-789", status=STATUS_PENDING, result="" ) m_celery.signature.return_value = m_signature @@ -123,7 +124,7 @@ def test_archive_url(m_celery, m2, client_with_auth): assert m_signature.apply_async.call_count == 2 -@patch("app.web.endpoints.url.UserState") +@patch("app.web.routers.url.UserState") def test_archive_url_quotas(m1, client_with_auth): m_user_state = MagicMock() m1.return_value = m_user_state @@ -152,11 +153,11 @@ def test_archive_url_quotas(m1, client_with_auth): m_user_state.has_quota_max_monthly_mbs.assert_called_once() -@patch("app.web.endpoints.url.celery", return_value=MagicMock()) +@patch("app.web.routers.url.celery", return_value=MagicMock()) def test_archive_url_with_api_token(m_celery, client_with_token): m_signature = MagicMock() m_signature.apply_async.return_value = TaskResult( - id="123-456-789", status="PENDING", result="" + id="123-456-789", status=STATUS_PENDING, result="" ) m_celery.signature.return_value = m_signature response = client_with_token.post( @@ -274,7 +275,7 @@ def test_search_by_url(client_with_auth, client_with_token, db_session): assert len(response.json()) == 10 -@patch("app.web.endpoints.url.UserState") +@patch("app.web.routers.url.UserState") def test_search_no_read_access(mock_user_state, client_with_auth): mock_user_state.return_value.read = False mock_user_state.return_value.read_public = False diff --git a/app/tests/web/test_main.py b/app/tests/web/test_main.py index 1b6e86b..a8a2c4f 100644 --- a/app/tests/web/test_main.py +++ b/app/tests/web/test_main.py @@ -40,7 +40,7 @@ def test_alembic(db_session): @patch( - "app.web.endpoints.url.crud.soft_delete_archive", + "app.web.routers.url.crud.soft_delete_archive", side_effect=Exception("mocked error"), ) def test_logging_middleware(m1, client_with_auth): diff --git a/app/web/main.py b/app/web/main.py index 525eff4..df5ca73 100644 --- a/app/web/main.py +++ b/app/web/main.py @@ -9,13 +9,13 @@ from prometheus_fastapi_instrumentator import Instrumentator from app.shared.settings import Settings, get_settings from app.shared.task_messaging import get_celery from app.web.config import API_DESCRIPTION, VERSION -from app.web.endpoints.default import default_router -from app.web.endpoints.interoperability import interoperability_router -from app.web.endpoints.sheet import sheet_router -from app.web.endpoints.task import task_router -from app.web.endpoints.url import url_router from app.web.events import lifespan from app.web.middleware import logging_middleware +from app.web.routers.default import router as default_router +from app.web.routers.interoperability import router as interoperability_router +from app.web.routers.sheet import router as sheet_router +from app.web.routers.task import router as task_router +from app.web.routers.url import router as url_router from app.web.security import token_api_key_auth diff --git a/app/web/endpoints/__init__.py b/app/web/routers/__init__.py similarity index 100% rename from app/web/endpoints/__init__.py rename to app/web/routers/__init__.py diff --git a/app/web/endpoints/default.py b/app/web/routers/default.py similarity index 88% rename from app/web/endpoints/default.py rename to app/web/routers/default.py index ce3edd2..0588ea8 100644 --- a/app/web/endpoints/default.py +++ b/app/web/routers/default.py @@ -11,22 +11,22 @@ from app.web.db.user_state import UserState from app.web.security import get_user_state -default_router = APIRouter() +router = APIRouter() -@default_router.get("/") +@router.get("/") async def home() -> JSONResponse: return JSONResponse( {"version": VERSION, "breakingChanges": BREAKING_CHANGES} ) -@default_router.get("/health") +@router.get("/health") async def health() -> JSONResponse: return JSONResponse({"status": "ok"}) -@default_router.get( +@router.get( "/user/active", summary="Check if the user is active and can use the tool." ) async def active( @@ -35,7 +35,7 @@ async def active( return ActiveUser(active=user.active) -@default_router.get( +@router.get( "/user/permissions", summary="Get the user's global 'all' permissions and the permissions for each group they belong to.", ) @@ -45,7 +45,7 @@ def get_user_permissions( return user.permissions -@default_router.get( +@router.get( "/user/usage", summary="Get the user's monthly URLs/MBs usage along with the total active sheets, breakdown by group.", ) @@ -59,6 +59,6 @@ def get_user_usage( return user.usage() -@default_router.get("/favicon.ico", include_in_schema=False) +@router.get("/favicon.ico", include_in_schema=False) async def favicon() -> FileResponse: return FileResponse("app/web/static/favicon.ico") diff --git a/app/web/endpoints/interoperability.py b/app/web/routers/interoperability.py similarity index 94% rename from app/web/endpoints/interoperability.py rename to app/web/routers/interoperability.py index d08bf50..1292698 100644 --- a/app/web/endpoints/interoperability.py +++ b/app/web/routers/interoperability.py @@ -17,13 +17,11 @@ from app.web.security import token_api_key_auth from app.web.utils.misc import get_all_urls -interoperability_router = APIRouter( - prefix="/interop", tags=["Interoperability endpoints."] -) +router = APIRouter(prefix="/interop", tags=["Interoperability endpoints."]) # ----- endpoint to submit data archived elsewhere -@interoperability_router.post( +@router.post( "/submit-archive", status_code=HTTPStatus.CREATED, summary="Submit a manual archive entry, for data that was archived elsewhere.", diff --git a/app/web/endpoints/sheet.py b/app/web/routers/sheet.py similarity index 94% rename from app/web/endpoints/sheet.py rename to app/web/routers/sheet.py index 5247107..2a06bf3 100644 --- a/app/web/endpoints/sheet.py +++ b/app/web/routers/sheet.py @@ -18,14 +18,12 @@ from app.web.db.user_state import UserState from app.web.security import get_user_state -sheet_router = APIRouter( - prefix="/sheet", tags=["Google Spreadsheet operations"] -) +router = APIRouter(prefix="/sheet", tags=["Google Spreadsheet operations"]) celery = get_celery() -@sheet_router.post( +@router.post( "/create", status_code=HTTPStatus.CREATED, summary="Store a new Google Sheet for regular archiving.", @@ -69,7 +67,7 @@ def create_sheet( ) from e -@sheet_router.get( +@router.get( "/mine", status_code=HTTPStatus.OK, summary="Get the authenticated user's Google Sheets.", @@ -81,7 +79,7 @@ def get_user_sheets( return crud.get_user_sheets(db, user.email) -@sheet_router.delete("/{sheet_id}", summary="Delete a Google Sheet by ID.") +@router.delete("/{sheet_id}", summary="Delete a Google Sheet by ID.") def delete_sheet( sheet_id: str, user: UserState = Depends(get_user_state), @@ -92,7 +90,7 @@ def delete_sheet( ) -@sheet_router.post( +@router.post( "/{sheet_id}/archive", status_code=HTTPStatus.CREATED, summary="Trigger an archiving task for a GSheet you own.", diff --git a/app/web/endpoints/task.py b/app/web/routers/task.py similarity index 81% rename from app/web/endpoints/task.py rename to app/web/routers/task.py index 3581ab2..9b81429 100644 --- a/app/web/endpoints/task.py +++ b/app/web/routers/task.py @@ -4,18 +4,19 @@ from fastapi.encoders import jsonable_encoder from fastapi.responses import JSONResponse from app.shared import schemas +from app.shared.constants import STATUS_FAILURE from app.shared.log import log_error from app.shared.task_messaging import get_celery from app.web.security import get_token_or_user_auth from app.web.utils.misc import custom_jsonable_encoder -task_router = APIRouter(prefix="/task", tags=["Async task operations"]) +router = APIRouter(prefix="/task", tags=["Async task operations"]) celery = get_celery() -@task_router.get( +@router.get( "/{task_id}", summary="Check the status of an async task by its id, works for URLs and Sheet tasks.", ) @@ -24,7 +25,7 @@ def get_status( ) -> schemas.TaskResult: task = AsyncResult(task_id, app=celery) try: - if task.status == "FAILURE": + if task.status == STATUS_FAILURE: # *FAILURE* The task raised an exception, or has exceeded the retry limit. # The :attr:`result` attribute then contains the exception raised by # the task. @@ -43,5 +44,9 @@ def get_status( except Exception as e: log_error(e) return JSONResponse( - {"id": task_id, "status": "FAILURE", "result": {"error": str(e)}} + { + "id": task_id, + "status": STATUS_FAILURE, + "result": {"error": str(e)}, + } ) diff --git a/app/web/endpoints/url.py b/app/web/routers/url.py similarity index 94% rename from app/web/endpoints/url.py rename to app/web/routers/url.py index 10b0adc..dd3e7c5 100644 --- a/app/web/endpoints/url.py +++ b/app/web/routers/url.py @@ -18,12 +18,12 @@ from app.web.security import get_token_or_user_auth, get_user_state from app.web.utils.misc import convert_priority_to_queue_dict -url_router = APIRouter(prefix="/url", tags=["Single URL operations"]) +router = APIRouter(prefix="/url", tags=["Single URL operations"]) celery = get_celery() -@url_router.post( +@router.post( "/archive", status_code=HTTPStatus.CREATED, summary="Submit a single URL archive request, starts an archiving task.", @@ -77,7 +77,7 @@ def archive_url( ) -@url_router.get("/search", summary="Search for archive entries by URL.") +@router.get("/search", summary="Search for archive entries by URL.") def search_by_url( url: str, skip: int = 0, @@ -110,9 +110,7 @@ def search_by_url( ) -@url_router.delete( - "/{archive_id}", summary="Delete a single URL archive by id." -) +@router.delete("/{archive_id}", summary="Delete a single URL archive by id.") def delete_archive( archive_id: str, user: UserState = Depends(get_user_state), From a65784ea9dafd69c3d3feed6937e8ab8d860d77e Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:09:25 +0100 Subject: [PATCH 26/40] updates base dependencies --- poetry.lock | 1357 +++++++++++++++++++++++++----------------------- pyproject.toml | 1 - 2 files changed, 706 insertions(+), 652 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3d4c1ef..d090800 100644 --- a/poetry.lock +++ b/poetry.lock @@ -37,23 +37,23 @@ docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.1)"] [[package]] name = "alembic" -version = "1.14.1" +version = "1.15.2" description = "A database migration tool for SQLAlchemy." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["web"] files = [ - {file = "alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5"}, - {file = "alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"}, + {file = "alembic-1.15.2-py3-none-any.whl", hash = "sha256:2e76bd916d547f6900ec4bb5a90aeac1485d2c92536923d0b138c02b126edc53"}, + {file = "alembic-1.15.2.tar.gz", hash = "sha256:1c72391bbdeffccfe317eefba686cb9a3c078005478885413b95c3b26c57a8a7"}, ] [package.dependencies] Mako = "*" -SQLAlchemy = ">=1.3.0" -typing-extensions = ">=4" +SQLAlchemy = ">=1.4.0" +typing-extensions = ">=4.12" [package.extras] -tz = ["backports.zoneinfo ; python_version < \"3.9\"", "tzdata"] +tz = ["tzdata"] [[package]] name = "amqp" @@ -84,14 +84,14 @@ files = [ [[package]] name = "anyio" -version = "4.8.0" +version = "4.9.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" groups = ["dev", "web"] files = [ - {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, - {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, ] [package.dependencies] @@ -101,52 +101,40 @@ sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "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)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] -[[package]] -name = "asn1crypto" -version = "1.5.1" -description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, - {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, -] - [[package]] name = "attrs" -version = "25.1.0" +version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, - {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "authlib" -version = "1.4.1" +version = "1.5.2" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "Authlib-1.4.1-py2.py3-none-any.whl", hash = "sha256:edc29c3f6a3e72cd9e9f45fff67fc663a2c364022eb0371c003f22d5405915c1"}, - {file = "authlib-1.4.1.tar.gz", hash = "sha256:30ead9ea4993cdbab821dc6e01e818362f92da290c04c7f6a1940f86507a790d"}, + {file = "authlib-1.5.2-py2.py3-none-any.whl", hash = "sha256:8804dd4402ac5e4a0435ac49e0b6e19e395357cfa632a3f624dcb4f6df13b4b1"}, + {file = "authlib-1.5.2.tar.gz", hash = "sha256:fe85ec7e50c5f86f1e2603518bb3b4f632985eb4a355e52256530790e326c512"}, ] [package.dependencies] @@ -154,22 +142,22 @@ cryptography = "*" [[package]] name = "auto-archiver" -version = "0.13.4" +version = "1.0.0" description = "Automatically archive links to videos, images, and social media content from Google Sheets (and more)." optional = false python-versions = "<3.13,>=3.10" groups = ["main"] files = [ - {file = "auto_archiver-0.13.4-py3-none-any.whl", hash = "sha256:490ee0dbc86e3481ee06cdbfbbaf397cbc9733b4aaac8cac233f29af5dc4ba53"}, - {file = "auto_archiver-0.13.4.tar.gz", hash = "sha256:dac206f643e8101bb1efdea2e6cbdfaca1e3ae50cfe3fa34b466b7518337d675"}, + {file = "auto_archiver-1.0.0-py3-none-any.whl", hash = "sha256:b4c20a3cff9a4e98815c858b662b10cd0afc7c80bad1e4adacbd33cfafc97855"}, + {file = "auto_archiver-1.0.0.tar.gz", hash = "sha256:2ce5b40c87bd67661357154ee5f06957b956396bc39f293c370e8237e23bae4e"}, ] [package.dependencies] beautifulsoup4 = ">=0.0.0" +bgutil-ytdlp-pot-provider = ">=0.7.3,<0.8.0" boto3 = ">=1.28.0,<2.0.0" bs4 = ">=0.0.0" -certvalidator = ">=0.0.0" -cryptography = ">=41.0.0,<42.0.0" +cryptography = ">44.0.1,<45.0.0" dataclasses-json = ">=0.0.0" dateparser = ">=0.0.0" ffmpeg-python = ">=0.0.0" @@ -183,23 +171,23 @@ jsonlines = ">=0.0.0" loguru = ">=0.0.0" numpy = "2.1.3" oauth2client = ">=0.0.0" +opentimestamps = ">=0.4.5,<0.5.0" pdqhash = ">=0.0.0" pillow = ">=0.0.0" -pyOpenSSL = "24.2.1" pysubs2 = ">=0.0.0" python-slugify = ">=0.0.0" python-twitter-v2 = ">=0.0.0" requests = {version = ">=0.0.0", extras = ["socks"]} retrying = ">=0.0.0" +rfc3161-client = ">=1.0.1,<2.0.0" rich-argparse = ">=1.6.0,<2.0.0" ruamel-yaml = ">=0.18.10,<0.19.0" selenium = ">=0.0.0" telethon = ">=0.0.0" tqdm = ">=0.0.0" -tsp-client = ">=0.0.0" vk-url-scraper = ">=0.0.0" warcio = ">=0.0.0" -yt-dlp = ">=2025.1.26,<2026.0.0" +yt-dlp = ">=2025.3.21,<2026.0.0" [[package]] name = "beautifulsoup4" @@ -224,6 +212,21 @@ charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "bgutil-ytdlp-pot-provider" +version = "0.7.4" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bgutil_ytdlp_pot_provider-0.7.4-py3-none-any.whl", hash = "sha256:5f0b1d884fec66dff703c421ea06f5fc9b11022d9c0babdaa0cab13ed99b9d77"}, + {file = "bgutil_ytdlp_pot_provider-0.7.4.tar.gz", hash = "sha256:b6c1462b8f979540078085cd82462ef967b8b70cd0810d469243a31f5081e5c6"}, +] + +[package.dependencies] +yt-dlp-get-pot = ">=0.1.1" + [[package]] name = "billiard" version = "4.2.1" @@ -250,18 +253,18 @@ files = [ [[package]] name = "boto3" -version = "1.36.26" +version = "1.37.25" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "boto3-1.36.26-py3-none-any.whl", hash = "sha256:f67d014a7c5a3cd540606d64d7cb9eec3600cf42acab1ac0518df9751ae115e2"}, - {file = "boto3-1.36.26.tar.gz", hash = "sha256:523b69457eee55ac15aa707c0e768b2a45ca1521f95b2442931090633ec72458"}, + {file = "boto3-1.37.25-py3-none-any.whl", hash = "sha256:00a025c621198508dc20c45224baaa7bd2a695323d999cce08b0d4deab5ada6f"}, + {file = "boto3-1.37.25.tar.gz", hash = "sha256:23e9cbad028ef3723567f4556411ee8d0f732594316b4c78c174a03ba3ca3159"}, ] [package.dependencies] -botocore = ">=1.36.26,<1.37.0" +botocore = ">=1.37.25,<1.38.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.11.0,<0.12.0" @@ -270,14 +273,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.36.26" +version = "1.37.25" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "botocore-1.36.26-py3-none-any.whl", hash = "sha256:4e3f19913887a58502e71ef8d696fe7eaa54de7813ff73390cd5883f837dfa6e"}, - {file = "botocore-1.36.26.tar.gz", hash = "sha256:4a63bcef7ecf6146fd3a61dc4f9b33b7473b49bdaf1770e9aaca6eee0c9eab62"}, + {file = "botocore-1.37.25-py3-none-any.whl", hash = "sha256:e35f10df0c3bcf42f4680439148462073fe6445d8938679f0576eb189fb034d7"}, + {file = "botocore-1.37.25.tar.gz", hash = "sha256:6f8cefd769df170809816d66bde2e12c43f557ca6cf18c807922003319b52991"}, ] [package.dependencies] @@ -474,60 +477,60 @@ files = [ [[package]] name = "celery" -version = "5.4.0" +version = "5.5.0" description = "Distributed Task Queue." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, - {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, + {file = "celery-5.5.0-py3-none-any.whl", hash = "sha256:f4170c6e5952281318448a899d9e9a15b9cbd007e002091766900dc8f71b9394"}, + {file = "celery-5.5.0.tar.gz", hash = "sha256:10d49f9926d16237310109b0e6e1e2f7a2133b84e684bb36534d7663e66919bb"}, ] [package.dependencies] -billiard = ">=4.2.0,<5.0" +billiard = ">=4.2.1,<5.0" click = ">=8.1.2,<9.0" click-didyoumean = ">=0.3.0" click-plugins = ">=1.1.1" click-repl = ">=0.2.0" -kombu = ">=5.3.4,<6.0" +kombu = ">=5.5.2,<5.6" python-dateutil = ">=2.8.2" -tzdata = ">=2022.7" vine = ">=5.1.0,<6.0" [package.extras] arangodb = ["pyArango (>=2.0.2)"] -auth = ["cryptography (==42.0.5)"] -azureblockblob = ["azure-storage-blob (>=12.15.0)"] +auth = ["cryptography (==44.0.2)"] +azureblockblob = ["azure-identity (>=1.19.0)", "azure-storage-blob (>=12.15.0)"] brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] cassandra = ["cassandra-driver (>=3.25.0,<4)"] consul = ["python-consul2 (==0.1.5)"] cosmosdbsql = ["pydocumentdb (==2.3.5)"] couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] -couchdb = ["pycouchdb (==1.14.2)"] +couchdb = ["pycouchdb (==1.16.0)"] django = ["Django (>=2.2.28)"] dynamodb = ["boto3 (>=1.26.143)"] -elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] +elasticsearch = ["elastic-transport (<=8.17.1)", "elasticsearch (<=8.17.2)"] eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] -gcs = ["google-cloud-storage (>=2.10.0)"] +gcs = ["google-cloud-firestore (==2.20.1)", "google-cloud-storage (>=2.10.0)", "grpcio (==1.67.0)"] gevent = ["gevent (>=1.5.0)"] librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] -mongodb = ["pymongo[srv] (>=4.0.2)"] -msgpack = ["msgpack (==1.0.8)"] +mongodb = ["pymongo (==4.10.1)"] +msgpack = ["msgpack (==1.1.0)"] +pydantic = ["pydantic (>=2.4)"] pymemcache = ["python-memcached (>=1.61)"] pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] -pytest = ["pytest-celery[all] (>=1.0.0)"] +pytest = ["pytest-celery[all] (>=1.2.0,<1.3.0)"] redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] s3 = ["boto3 (>=1.26.143)"] -slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem (==4.1.5) ; platform_python_implementation != \"PyPy\""] +slmq = ["softlayer_messaging (>=1.0.3)"] +solar = ["ephem (==4.2) ; platform_python_implementation != \"PyPy\""] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "urllib3 (>=1.26.16)"] tblib = ["tblib (>=1.3.0) ; python_version < \"3.8.0\"", "tblib (>=1.5.0) ; python_version >= \"3.8.0\""] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] -zstd = ["zstandard (==0.22.0)"] +zstd = ["zstandard (==0.23.0)"] [[package]] name = "certifi" @@ -541,22 +544,6 @@ files = [ {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] -[[package]] -name = "certvalidator" -version = "0.11.1" -description = "Validates X.509 certificates and paths" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "certvalidator-0.11.1-py2.py3-none-any.whl", hash = "sha256:77520b269f516d4fb0902998d5bd0eb3727fe153b659aa1cb828dcf12ea6b8de"}, - {file = "certvalidator-0.11.1.tar.gz", hash = "sha256:922d141c94393ab285ca34338e18dd4093e3ae330b1f278e96c837cb62cffaad"}, -] - -[package.dependencies] -asn1crypto = ">=0.18.1" -oscrypto = ">=0.16.1" - [[package]] name = "cffi" version = "1.17.1" @@ -633,6 +620,7 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] +markers = {main = "os_name == \"nt\" and implementation_name != \"pypy\" or platform_python_implementation != \"PyPy\"", web = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = "*" @@ -833,75 +821,75 @@ markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", [[package]] name = "coverage" -version = "7.6.12" +version = "7.8.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, - {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, - {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, - {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, - {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, - {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, - {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, - {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, - {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, - {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, - {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, - {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, - {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, - {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, - {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, - {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, - {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, ] [package.extras] @@ -909,48 +897,60 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "41.0.7" +version = "44.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.7" +python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main", "web"] files = [ - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, - {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, - {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, - {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, + {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"}, + {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"}, + {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"}, + {file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"}, + {file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"}, + {file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"}, + {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"}, + {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"}, + {file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"}, + {file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"}, + {file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"}, ] [package.dependencies] -cffi = ">=1.12" +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -nox = ["nox"] -pep8test = ["black", "check-sdist", "mypy", "ruff"] -sdist = ["build"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] +pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -1059,19 +1059,19 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.115.8" +version = "0.115.12" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" groups = ["web"] files = [ - {file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"}, - {file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"}, + {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, + {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, ] [package.dependencies] pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.46.0" +starlette = ">=0.40.0,<0.47.0" typing-extensions = ">=4.8.0" [package.extras] @@ -1144,14 +1144,14 @@ dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4 [[package]] name = "filelock" -version = "3.17.0" +version = "3.18.0" description = "A platform independent file lock." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, - {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] [package.extras] @@ -1193,19 +1193,19 @@ files = [ [[package]] name = "google-api-core" -version = "2.24.1" +version = "2.24.2" description = "Google API client core library" optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1"}, - {file = "google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a"}, + {file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"}, + {file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"}, ] [package.dependencies] -google-auth = ">=2.14.1,<3.0.dev0" -googleapis-common-protos = ">=1.56.2,<2.0.dev0" +google-auth = ">=2.14.1,<3.0.0" +googleapis-common-protos = ">=1.56.2,<2.0.0" grpcio = [ {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, @@ -1214,9 +1214,9 @@ grpcio-status = [ {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, ] -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" -requests = ">=2.18.0,<3.0.0.dev0" +proto-plus = ">=1.22.3,<2.0.0" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" +requests = ">=2.18.0,<3.0.0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] @@ -1226,21 +1226,21 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.161.0" +version = "2.166.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "google_api_python_client-2.161.0-py2.py3-none-any.whl", hash = "sha256:9476a5a4f200bae368140453df40f9cda36be53fa7d0e9a9aac4cdb859a26448"}, - {file = "google_api_python_client-2.161.0.tar.gz", hash = "sha256:324c0cce73e9ea0a0d2afd5937e01b7c2d6a4d7e2579cdb6c384f9699d6c9f37"}, + {file = "google_api_python_client-2.166.0-py2.py3-none-any.whl", hash = "sha256:dd8cc74d9fc18538ab05cbd2e93cb4f82382f910c5f6945db06c91f1deae6e45"}, + {file = "google_api_python_client-2.166.0.tar.gz", hash = "sha256:b8cf843bd9d736c134aef76cf1dc7a47c9283a2ef24267b97207b9dd43b30ef7"}, ] [package.dependencies] -google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" -google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0" +google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" google-auth-httplib2 = ">=0.2.0,<1.0.0" -httplib2 = ">=0.19.0,<1.dev0" +httplib2 = ">=0.19.0,<1.0.0" uritemplate = ">=3.0.1,<5" [[package]] @@ -1371,39 +1371,46 @@ tracing = ["opentelemetry-api (>=1.1.0)"] [[package]] name = "google-crc32c" -version = "1.6.0" +version = "1.7.1" description = "A python wrapper of the C library 'Google CRC32C'" optional = false python-versions = ">=3.9" groups = ["web"] files = [ - {file = "google_crc32c-1.6.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa"}, - {file = "google_crc32c-1.6.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9"}, - {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7"}, - {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e"}, - {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc"}, - {file = "google_crc32c-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42"}, - {file = "google_crc32c-1.6.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4"}, - {file = "google_crc32c-1.6.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8"}, - {file = "google_crc32c-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d"}, - {file = "google_crc32c-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f"}, - {file = "google_crc32c-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3"}, - {file = "google_crc32c-1.6.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d"}, - {file = "google_crc32c-1.6.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b"}, - {file = "google_crc32c-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00"}, - {file = "google_crc32c-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3"}, - {file = "google_crc32c-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760"}, - {file = "google_crc32c-1.6.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205"}, - {file = "google_crc32c-1.6.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0"}, - {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2"}, - {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871"}, - {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57"}, - {file = "google_crc32c-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c"}, - {file = "google_crc32c-1.6.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc"}, - {file = "google_crc32c-1.6.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d"}, - {file = "google_crc32c-1.6.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24"}, - {file = "google_crc32c-1.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d"}, - {file = "google_crc32c-1.6.0.tar.gz", hash = "sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc"}, + {file = "google_crc32c-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76"}, + {file = "google_crc32c-1.7.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603"}, + {file = "google_crc32c-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a"}, + {file = "google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06"}, + {file = "google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9"}, + {file = "google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77"}, + {file = "google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53"}, + {file = "google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d"}, + {file = "google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194"}, + {file = "google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e"}, + {file = "google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337"}, + {file = "google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65"}, + {file = "google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6"}, + {file = "google_crc32c-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35"}, + {file = "google_crc32c-1.7.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638"}, + {file = "google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb"}, + {file = "google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6"}, + {file = "google_crc32c-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db"}, + {file = "google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3"}, + {file = "google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9"}, + {file = "google_crc32c-1.7.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315"}, + {file = "google_crc32c-1.7.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582"}, + {file = "google_crc32c-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349"}, + {file = "google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589"}, + {file = "google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b"}, + {file = "google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48"}, + {file = "google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82"}, + {file = "google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472"}, ] [package.extras] @@ -1430,21 +1437,21 @@ requests = ["requests (>=2.18.0,<3.0.0dev)"] [[package]] name = "googleapis-common-protos" -version = "1.68.0" +version = "1.69.2" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "googleapis_common_protos-1.68.0-py2.py3-none-any.whl", hash = "sha256:aaf179b2f81df26dfadac95def3b16a95064c76a5f45f07e4c68a21bb371c4ac"}, - {file = "googleapis_common_protos-1.68.0.tar.gz", hash = "sha256:95d38161f4f9af0d9423eed8fb7b64ffd2568c3464eb542ff02c5bfa1953ab3c"}, + {file = "googleapis_common_protos-1.69.2-py3-none-any.whl", hash = "sha256:0b30452ff9c7a27d80bfc5718954063e8ab53dd3697093d3bc99581f5fd24212"}, + {file = "googleapis_common_protos-1.69.2.tar.gz", hash = "sha256:3e1b904a27a33c821b4b749fd31d334c0c9c30e6113023d495e48979a3dc9c5f"}, ] [package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" [package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] [[package]] name = "greenlet" @@ -1619,14 +1626,14 @@ protobuf = ">=5.26.1,<6.0dev" [[package]] name = "gspread" -version = "6.1.4" +version = "6.2.0" description = "Google Spreadsheets Python API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "gspread-6.1.4-py3-none-any.whl", hash = "sha256:c34781c426031a243ad154952b16f21ac56a5af90687885fbee3d1fba5280dcd"}, - {file = "gspread-6.1.4.tar.gz", hash = "sha256:b8eec27de7cadb338bb1b9f14a9be168372dee8965c0da32121816b5050ac1de"}, + {file = "gspread-6.2.0-py3-none-any.whl", hash = "sha256:7fa1a11e1ecacc6c5946fa016be05941baca8540404314f59aec963dd8ae5db3"}, + {file = "gspread-6.2.0.tar.gz", hash = "sha256:bc3d02d1c39e0b40bfc8035b4fec407aa71a17f343fc81cc7e3f75bfa6555de6"}, ] [package.dependencies] @@ -1709,14 +1716,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "identify" -version = "2.6.8" +version = "2.6.9" description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "identify-2.6.8-py2.py3-none-any.whl", hash = "sha256:83657f0f766a3c8d0eaea16d4ef42494b39b34629a4b3192a9d020d349b3e255"}, - {file = "identify-2.6.8.tar.gz", hash = "sha256:61491417ea2c0c5c670484fd8abbb34de34cdae1e5f39a73ee65e48e4bb663fc"}, + {file = "identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150"}, + {file = "identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf"}, ] [package.extras] @@ -1739,14 +1746,14 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -1769,14 +1776,14 @@ browser-cookie3 = ["browser_cookie3 (>=0.19.1)"] [[package]] name = "jinja2" -version = "3.1.5" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -1814,19 +1821,19 @@ attrs = ">=19.2.0" [[package]] name = "kombu" -version = "5.4.2" +version = "5.5.2" description = "Messaging library for Python." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763"}, - {file = "kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"}, + {file = "kombu-5.5.2-py3-none-any.whl", hash = "sha256:40f3674ed19603b8a771b6c74de126dbf8879755a0337caac6602faa82d539cd"}, + {file = "kombu-5.5.2.tar.gz", hash = "sha256:2dd27ec84fd843a4e0a7187424313f87514b344812cb98c25daddafbb6a7ff0e"}, ] [package.dependencies] amqp = ">=5.1.1,<6.0.0" -tzdata = {version = "*", markers = "python_version >= \"3.9\""} +tzdata = {version = "2025.2", markers = "python_version >= \"3.9\""} vine = "5.1.0" [package.extras] @@ -1834,15 +1841,16 @@ azureservicebus = ["azure-servicebus (>=7.10.0)"] azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] confluentkafka = ["confluent-kafka (>=2.2.0)"] consul = ["python-consul2 (==0.1.5)"] +gcpubsub = ["google-cloud-monitoring (>=2.16.0)", "google-cloud-pubsub (>=2.18.4)", "grpcio (==1.67.0)", "protobuf (==4.25.5)"] librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] mongodb = ["pymongo (>=4.1.1)"] msgpack = ["msgpack (==1.1.0)"] pyro = ["pyro4 (==4.82)"] qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] -redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] +redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2,<=5.2.1)"] slmq = ["softlayer-messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] +sqs = ["boto3 (>=1.26.143)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] @@ -2225,23 +2233,20 @@ signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] -name = "oscrypto" -version = "1.3.0" -description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD." +name = "opentimestamps" +version = "0.4.5" +description = "Create and verify OpenTimestamps proofs" optional = false python-versions = "*" groups = ["main"] -files = [] -develop = false +files = [ + {file = "opentimestamps-0.4.5-py3-none-any.whl", hash = "sha256:a4912b3bd1b612a3ef5fac925b9137889e6c5cb91cc9e76c8202a2bf8abe26b5"}, + {file = "opentimestamps-0.4.5.tar.gz", hash = "sha256:56726ccde97fb67f336a7f237ce36808e5593c3089d68d900b1c83d0ebf9dcfa"}, +] [package.dependencies] -asn1crypto = ">=1.5.1" - -[package.source] -type = "git" -url = "https://github.com/wbond/oscrypto.git" -reference = "d5f3437ed24257895ae1edd9e503cfb352e635a8" -resolved_reference = "d5f3437ed24257895ae1edd9e503cfb352e635a8" +pycryptodomex = ">=3.3.1" +python-bitcoinlib = ">=0.9.0,<0.13.0" [[package]] name = "outcome" @@ -2382,20 +2387,20 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.7" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" @@ -2415,14 +2420,14 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "4.1.0" +version = "4.2.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, - {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, + {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, + {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, ] [package.dependencies] @@ -2449,14 +2454,14 @@ twisted = ["twisted"] [[package]] name = "prometheus-fastapi-instrumentator" -version = "7.0.2" +version = "7.1.0" description = "Instrument your FastAPI app with Prometheus metrics" optional = false python-versions = ">=3.8" groups = ["web"] files = [ - {file = "prometheus_fastapi_instrumentator-7.0.2-py3-none-any.whl", hash = "sha256:975e39992acb7a112758ff13ba95317e6c54d1bbf605f9156f31ac9f2800c32d"}, - {file = "prometheus_fastapi_instrumentator-7.0.2.tar.gz", hash = "sha256:8a4d8fb13dbe19d2882ac6af9ce236e4e1f98dc48e3fa44fe88d8e23ac3c953f"}, + {file = "prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9"}, + {file = "prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e"}, ] [package.dependencies] @@ -2480,41 +2485,41 @@ wcwidth = "*" [[package]] name = "proto-plus" -version = "1.26.0" +version = "1.26.1" description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"}, - {file = "proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22"}, + {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, + {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, ] [package.dependencies] -protobuf = ">=3.19.0,<6.0.0dev" +protobuf = ">=3.19.0,<7.0.0" [package.extras] testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "5.29.3" +version = "5.29.4" description = "" optional = false python-versions = ">=3.8" groups = ["main", "web"] files = [ - {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, - {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, - {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, - {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, - {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, - {file = "protobuf-5.29.3-cp38-cp38-win32.whl", hash = "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252"}, - {file = "protobuf-5.29.3-cp38-cp38-win_amd64.whl", hash = "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107"}, - {file = "protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7"}, - {file = "protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da"}, - {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, - {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, + {file = "protobuf-5.29.4-cp310-abi3-win32.whl", hash = "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7"}, + {file = "protobuf-5.29.4-cp310-abi3-win_amd64.whl", hash = "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d"}, + {file = "protobuf-5.29.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:307ecba1d852ec237e9ba668e087326a67564ef83e45a0189a772ede9e854dd0"}, + {file = "protobuf-5.29.4-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e"}, + {file = "protobuf-5.29.4-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922"}, + {file = "protobuf-5.29.4-cp38-cp38-win32.whl", hash = "sha256:1832f0515b62d12d8e6ffc078d7e9eb06969aa6dc13c13e1036e39d73bebc2de"}, + {file = "protobuf-5.29.4-cp38-cp38-win_amd64.whl", hash = "sha256:476cb7b14914c780605a8cf62e38c2a85f8caff2e28a6a0bad827ec7d6c85d68"}, + {file = "protobuf-5.29.4-cp39-cp39-win32.whl", hash = "sha256:fd32223020cb25a2cc100366f1dedc904e2d71d9322403224cdde5fdced0dabe"}, + {file = "protobuf-5.29.4-cp39-cp39-win_amd64.whl", hash = "sha256:678974e1e3a9b975b8bc2447fca458db5f93a2fb6b0c8db46b6675b5b5346812"}, + {file = "protobuf-5.29.4-py3-none-any.whl", hash = "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862"}, + {file = "protobuf-5.29.4.tar.gz", hash = "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99"}, ] [[package]] @@ -2571,18 +2576,18 @@ files = [ [[package]] name = "pyasn1-modules" -version = "0.4.1" +version = "0.4.2" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" groups = ["main", "web"] files = [ - {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, - {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.7.0" +pyasn1 = ">=0.6.1,<0.7.0" [[package]] name = "pycparser" @@ -2595,65 +2600,64 @@ files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +markers = {main = "os_name == \"nt\" and implementation_name != \"pypy\" or platform_python_implementation != \"PyPy\"", web = "platform_python_implementation != \"PyPy\""} [[package]] name = "pycryptodomex" -version = "3.21.0" +version = "3.22.0" description = "Cryptographic library for Python" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main"] files = [ - {file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"}, - {file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"}, - {file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"}, - {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:41673e5cc39a8524557a0472077635d981172182c9fe39ce0b5f5c19381ffaff"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:276be1ed006e8fd01bba00d9bd9b60a0151e478033e86ea1cb37447bbc057edc"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:813e57da5ceb4b549bab96fa548781d9a63f49f1d68fdb148eeac846238056b7"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-win32.whl", hash = "sha256:d7beeacb5394765aa8dabed135389a11ee322d3ee16160d178adc7f8ee3e1f65"}, + {file = "pycryptodomex-3.22.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:b3746dedf74787da43e4a2f85bd78f5ec14d2469eb299ddce22518b3891f16ea"}, + {file = "pycryptodomex-3.22.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5ebc09b7d8964654aaf8a4f5ac325f2b0cc038af9bea12efff0cd4a5bb19aa42"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:aef4590263b9f2f6283469e998574d0bd45c14fb262241c27055b82727426157"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5ac608a6dce9418d4f300fab7ba2f7d499a96b462f2b9b5c90d8d994cd36dcad"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a24f681365ec9757ccd69b85868bbd7216ba451d0f86f6ea0eed75eeb6975db"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:259664c4803a1fa260d5afb322972813c5fe30ea8b43e54b03b7e3a27b30856b"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7127d9de3c7ce20339e06bcd4f16f1a1a77f1471bcf04e3b704306dde101b719"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee75067b35c93cc18b38af47b7c0664998d8815174cfc66dd00ea1e244eb27e6"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:1a8b0c5ba061ace4bcd03496d42702c3927003db805b8ec619ea6506080b381d"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bfe4fe3233ef3e58028a3ad8f28473653b78c6d56e088ea04fe7550c63d4d16b"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-win32.whl", hash = "sha256:2cac9ed5c343bb3d0075db6e797e6112514764d08d667c74cb89b931aac9dddd"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:ff46212fda7ee86ec2f4a64016c994e8ad80f11ef748131753adb67e9b722ebd"}, + {file = "pycryptodomex-3.22.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:5bf3ce9211d2a9877b00b8e524593e2209e370a287b3d5e61a8c45f5198487e2"}, + {file = "pycryptodomex-3.22.0-pp27-pypy_73-win32.whl", hash = "sha256:684cb57812cd243217c3d1e01a720c5844b30f0b7b64bb1a49679f7e1e8a54ac"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c8cffb03f5dee1026e3f892f7cffd79926a538c67c34f8b07c90c0bd5c834e27"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:140b27caa68a36d0501b05eb247bd33afa5f854c1ee04140e38af63c750d4e39"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:644834b1836bb8e1d304afaf794d5ae98a1d637bd6e140c9be7dd192b5374811"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c506aba3318505dbeecf821ed7b9a9f86f422ed085e2d79c4fba0ae669920a"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7cd39f7a110c1ab97ce9ee3459b8bc615920344dc00e56d1b709628965fba3f2"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e4eaaf6163ff13788c1f8f615ad60cdc69efac6d3bf7b310b21e8cfe5f46c801"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eac39e237d65981554c2d4c6668192dc7051ad61ab5fc383ed0ba049e4007ca2"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ab0d89d1761959b608952c7b347b0e76a32d1a5bb278afbaa10a7f3eaef9a0a"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e64164f816f5e43fd69f8ed98eb28f98157faf68208cd19c44ed9d8e72d33e8"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f005de31efad6f9acefc417296c641f13b720be7dbfec90edeaca601c0fab048"}, + {file = "pycryptodomex-3.22.0.tar.gz", hash = "sha256:a1da61bacc22f93a91cbe690e3eb2022a03ab4123690ab16c46abb693a9df63d"}, ] [[package]] name = "pydantic" -version = "2.10.6" +version = "2.11.1" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "web"] files = [ - {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, - {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, + {file = "pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8"}, + {file = "pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" +pydantic-core = "2.33.0" typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -2661,112 +2665,111 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.0" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "web"] files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, + {file = "pydantic_core-2.33.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71dffba8fe9ddff628c68f3abd845e91b028361d43c5f8e7b3f8b91d7d85413e"}, + {file = "pydantic_core-2.33.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:abaeec1be6ed535a5d7ffc2e6c390083c425832b20efd621562fbb5bff6dc518"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759871f00e26ad3709efc773ac37b4d571de065f9dfb1778012908bcc36b3a73"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcfebee69cd5e1c0b76a17e17e347c84b00acebb8dd8edb22d4a03e88e82a207"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b1262b912435a501fa04cd213720609e2cefa723a07c92017d18693e69bf00b"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4726f1f3f42d6a25678c67da3f0b10f148f5655813c5aca54b0d1742ba821b8f"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e790954b5093dff1e3a9a2523fddc4e79722d6f07993b4cd5547825c3cbf97b5"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34e7fb3abe375b5c4e64fab75733d605dda0f59827752debc99c17cb2d5f3276"}, + {file = "pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ecb158fb9b9091b515213bed3061eb7deb1d3b4e02327c27a0ea714ff46b0760"}, + {file = "pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:4d9149e7528af8bbd76cc055967e6e04617dcb2a2afdaa3dea899406c5521faa"}, + {file = "pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e81a295adccf73477220e15ff79235ca9dcbcee4be459eb9d4ce9a2763b8386c"}, + {file = "pydantic_core-2.33.0-cp310-cp310-win32.whl", hash = "sha256:f22dab23cdbce2005f26a8f0c71698457861f97fc6318c75814a50c75e87d025"}, + {file = "pydantic_core-2.33.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cb2390355ba084c1ad49485d18449b4242da344dea3e0fe10babd1f0db7dcfc"}, + {file = "pydantic_core-2.33.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a608a75846804271cf9c83e40bbb4dab2ac614d33c6fd5b0c6187f53f5c593ef"}, + {file = "pydantic_core-2.33.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c69aa459f5609dec2fa0652d495353accf3eda5bdb18782bc5a2ae45c9273a"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ec80eb5a5f45a2211793f1c4aeddff0c3761d1c70d684965c1807e923a588b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e925819a98318d17251776bd3d6aa9f3ff77b965762155bdad15d1a9265c4cfd"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bf68bb859799e9cec3d9dd8323c40c00a254aabb56fe08f907e437005932f2b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b2ea72dea0825949a045fa4071f6d5b3d7620d2a208335207793cf29c5a182d"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1583539533160186ac546b49f5cde9ffc928062c96920f58bd95de32ffd7bffd"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23c3e77bf8a7317612e5c26a3b084c7edeb9552d645742a54a5867635b4f2453"}, + {file = "pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7a7f2a3f628d2f7ef11cb6188bcf0b9e1558151d511b974dfea10a49afe192b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:f1fb026c575e16f673c61c7b86144517705865173f3d0907040ac30c4f9f5915"}, + {file = "pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:635702b2fed997e0ac256b2cfbdb4dd0bf7c56b5d8fba8ef03489c03b3eb40e2"}, + {file = "pydantic_core-2.33.0-cp311-cp311-win32.whl", hash = "sha256:07b4ced28fccae3f00626eaa0c4001aa9ec140a29501770a88dbbb0966019a86"}, + {file = "pydantic_core-2.33.0-cp311-cp311-win_amd64.whl", hash = "sha256:4927564be53239a87770a5f86bdc272b8d1fbb87ab7783ad70255b4ab01aa25b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-win_arm64.whl", hash = "sha256:69297418ad644d521ea3e1aa2e14a2a422726167e9ad22b89e8f1130d68e1e9a"}, + {file = "pydantic_core-2.33.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6c32a40712e3662bebe524abe8abb757f2fa2000028d64cc5a1006016c06af43"}, + {file = "pydantic_core-2.33.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ec86b5baa36f0a0bfb37db86c7d52652f8e8aa076ab745ef7725784183c3fdd"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4deac83a8cc1d09e40683be0bc6d1fa4cde8df0a9bf0cda5693f9b0569ac01b6"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:175ab598fb457a9aee63206a1993874badf3ed9a456e0654273e56f00747bbd6"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f36afd0d56a6c42cf4e8465b6441cf546ed69d3a4ec92724cc9c8c61bd6ecf4"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a98257451164666afafc7cbf5fb00d613e33f7e7ebb322fbcd99345695a9a61"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecc6d02d69b54a2eb83ebcc6f29df04957f734bcf309d346b4f83354d8376862"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a69b7596c6603afd049ce7f3835bcf57dd3892fc7279f0ddf987bebed8caa5a"}, + {file = "pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea30239c148b6ef41364c6f51d103c2988965b643d62e10b233b5efdca8c0099"}, + {file = "pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:abfa44cf2f7f7d7a199be6c6ec141c9024063205545aa09304349781b9a125e6"}, + {file = "pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20d4275f3c4659d92048c70797e5fdc396c6e4446caf517ba5cad2db60cd39d3"}, + {file = "pydantic_core-2.33.0-cp312-cp312-win32.whl", hash = "sha256:918f2013d7eadea1d88d1a35fd4a1e16aaf90343eb446f91cb091ce7f9b431a2"}, + {file = "pydantic_core-2.33.0-cp312-cp312-win_amd64.whl", hash = "sha256:aec79acc183865bad120b0190afac467c20b15289050648b876b07777e67ea48"}, + {file = "pydantic_core-2.33.0-cp312-cp312-win_arm64.whl", hash = "sha256:5461934e895968655225dfa8b3be79e7e927e95d4bd6c2d40edd2fa7052e71b6"}, + {file = "pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555"}, + {file = "pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181"}, + {file = "pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d"}, + {file = "pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3"}, + {file = "pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b"}, + {file = "pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585"}, + {file = "pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606"}, + {file = "pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225"}, + {file = "pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87"}, + {file = "pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b"}, + {file = "pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7"}, + {file = "pydantic_core-2.33.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7c9c84749f5787781c1c45bb99f433402e484e515b40675a5d121ea14711cf61"}, + {file = "pydantic_core-2.33.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:64672fa888595a959cfeff957a654e947e65bbe1d7d82f550417cbd6898a1d6b"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bc7367c0961dec292244ef2549afa396e72e28cc24706210bd44d947582c59"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce72d46eb201ca43994303025bd54d8a35a3fc2a3495fac653d6eb7205ce04f4"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14229c1504287533dbf6b1fc56f752ce2b4e9694022ae7509631ce346158de11"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:085d8985b1c1e48ef271e98a658f562f29d89bda98bf120502283efbc87313eb"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31860fbda80d8f6828e84b4a4d129fd9c4535996b8249cfb8c720dc2a1a00bb8"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f200b2f20856b5a6c3a35f0d4e344019f805e363416e609e9b47c552d35fd5ea"}, + {file = "pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f72914cfd1d0176e58ddc05c7a47674ef4222c8253bf70322923e73e14a4ac3"}, + {file = "pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:91301a0980a1d4530d4ba7e6a739ca1a6b31341252cb709948e0aca0860ce0ae"}, + {file = "pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7419241e17c7fbe5074ba79143d5523270e04f86f1b3a0dff8df490f84c8273a"}, + {file = "pydantic_core-2.33.0-cp39-cp39-win32.whl", hash = "sha256:7a25493320203005d2a4dac76d1b7d953cb49bce6d459d9ae38e30dd9f29bc9c"}, + {file = "pydantic_core-2.33.0-cp39-cp39-win_amd64.whl", hash = "sha256:82a4eba92b7ca8af1b7d5ef5f3d9647eee94d1f74d21ca7c21e3a2b92e008358"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2762c568596332fdab56b07060c8ab8362c56cf2a339ee54e491cd503612c50"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bf637300ff35d4f59c006fff201c510b2b5e745b07125458a5389af3c0dff8c"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c151ce3d59ed56ebd7ce9ce5986a409a85db697d25fc232f8e81f195aa39a1"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee65f0cc652261744fd07f2c6e6901c914aa6c5ff4dcfaf1136bc394d0dd26b"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:024d136ae44d233e6322027bbf356712b3940bee816e6c948ce4b90f18471b3d"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e37f10f6d4bc67c58fbd727108ae1d8b92b397355e68519f1e4a7babb1473442"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:502ed542e0d958bd12e7c3e9a015bce57deaf50eaa8c2e1c439b512cb9db1e3a"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:715c62af74c236bf386825c0fdfa08d092ab0f191eb5b4580d11c3189af9d330"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bccc06fa0372151f37f6b69834181aa9eb57cf8665ed36405fb45fbf6cac3bae"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d8dc9f63a26f7259b57f46a7aab5af86b2ad6fbe48487500bb1f4b27e051e4c"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30369e54d6d0113d2aa5aee7a90d17f225c13d87902ace8fcd7bbf99b19124db"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb479354c62067afa62f53bb387827bee2f75c9c79ef25eef6ab84d4b1ae3b"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310524c833d91403c960b8a3cf9f46c282eadd6afd276c8c5edc617bd705dc9"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eddb18a00bbb855325db27b4c2a89a4ba491cd6a0bd6d852b225172a1f54b36c"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ade5dbcf8d9ef8f4b28e682d0b29f3008df9842bb5ac48ac2c17bc55771cc976"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2c0afd34f928383e3fd25740f2050dbac9d077e7ba5adbaa2227f4d4f3c8da5c"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7da333f21cd9df51d5731513a6d39319892947604924ddf2e24a4612975fb936"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b6d77c75a57f041c5ee915ff0b0bb58eabb78728b69ed967bc5b780e8f701b8"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba95691cf25f63df53c1d342413b41bd7762d9acb425df8858d7efa616c0870e"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f1ab031feb8676f6bd7c85abec86e2935850bf19b84432c64e3e239bffeb1ec"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58c1151827eef98b83d49b6ca6065575876a02d2211f259fb1a6b7757bd24dd8"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66d931ea2c1464b738ace44b7334ab32a2fd50be023d863935eb00f42be1778"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0bcf0bab28995d483f6c8d7db25e0d05c3efa5cebfd7f56474359e7137f39856"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:89670d7a0045acb52be0566df5bc8b114ac967c662c06cf5e0c606e4aadc964b"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:b716294e721d8060908dbebe32639b01bfe61b15f9f57bcc18ca9a0e00d9520b"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fc53e05c16697ff0c1c7c2b98e45e131d4bfb78068fffff92a82d169cbb4c7b7"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:68504959253303d3ae9406b634997a2123a0b0c1da86459abbd0ffc921695eac"}, + {file = "pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3"}, ] [package.dependencies] @@ -2774,14 +2777,14 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.8.0" +version = "2.8.1" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" groups = ["main", "web"] files = [ - {file = "pydantic_settings-2.8.0-py3-none-any.whl", hash = "sha256:c782c7dc3fb40e97b238e713c25d26f64314aece2e91abcff592fcac15f71820"}, - {file = "pydantic_settings-2.8.0.tar.gz", hash = "sha256:88e2ca28f6e68ea102c99c3c401d6c9078e68a5df600e97b43891c34e089500a"}, + {file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"}, + {file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"}, ] [package.dependencies] @@ -2831,33 +2834,34 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pyopenssl" -version = "24.2.1" +version = "25.0.0" description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"}, - {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"}, + {file = "pyOpenSSL-25.0.0-py3-none-any.whl", hash = "sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90"}, + {file = "pyopenssl-25.0.0.tar.gz", hash = "sha256:cd2cef799efa3936bb08e8ccb9433a575722b9dd986023f1cabc4ae64e9dac16"}, ] [package.dependencies] -cryptography = ">=41.0.5,<44" +cryptography = ">=41.0.5,<45" +typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""} [package.extras] -docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] +docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] [[package]] name = "pyparsing" -version = "3.2.1" +version = "3.2.3" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" groups = ["main", "web"] files = [ - {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, - {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, + {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, + {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, ] [package.extras] @@ -2890,14 +2894,14 @@ files = [ [[package]] name = "pytest" -version = "8.3.4" +version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, ] [package.dependencies] @@ -2930,6 +2934,18 @@ pytest = ">=8.2,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "python-bitcoinlib" +version = "0.12.2" +description = "The Swiss Army Knife of the Bitcoin protocol." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "python-bitcoinlib-0.12.2.tar.gz", hash = "sha256:c65ab61427c77c38d397bfc431f71d86fd355b453a536496ec3fcb41bd10087d"}, + {file = "python_bitcoinlib-0.12.2-py3-none-any.whl", hash = "sha256:2f29a9f475f21c12169b3a6cc8820f34f11362d7ff1200a5703dce3e4e903a44"}, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2947,14 +2963,14 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.0" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "web"] files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, ] [package.extras] @@ -2997,14 +3013,14 @@ requests = ">=2.28" [[package]] name = "pytz" -version = "2025.1" +version = "2025.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, - {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] [[package]] @@ -3246,16 +3262,47 @@ files = [ [package.dependencies] six = ">=1.7.0" +[[package]] +name = "rfc3161-client" +version = "1.0.1" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "rfc3161_client-1.0.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:75d8c9d255fa79b9ae4aa27cee519893599efd79f9e6c24a1194dd296ce1c210"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0d3db059fe08d8b6b06aff89e133fcc352ffea1a1dafadb116dda9dae59d0689"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdef0c9d3213ca5b79d7f76ada48ae10c5011cb25abed2f6df07b344d16d1c28"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c34ce4d7d2bf5207c54de3a771e757f1f8bb04a8469d3cef6aefe074841064d"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4809f2fcfb5f8b42261a7b831929f62a297b584c8d1f4d242eae5e9447674b6"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a644b220b7f0f0be7856f49b043651982bd76e7aa9eb17b3e4e303fde36ed5a1"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:bb03a5a77b07adf766b7daac6cb8b7a8337ffc8f6d6046af74469973f52df8e1"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:d6c6e4626780b1c531d32d6a126d6c27865b1eb59c65e8b0f1f8f94aa3205285"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:912c2f049ce23d0f1c173b6fbd8673f964a27ad97907064dbc74f86dd0d95d15"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:081211a1b602b6dff7feb314d39ca2229c8db4e8cf55eef0c35b460470f4b2bb"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-win32.whl", hash = "sha256:59efa8fddf72a15e397276fe512dbfb99c0dc95032b495815bfc4f8f16302f2c"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:5381a63d5ed5b3c257cb18aacf3f737b1a1ad6df634290fe689b6d601c61cd24"}, + {file = "rfc3161_client-1.0.1.tar.gz", hash = "sha256:1c951f3912b90c6d3f3505e644b74ee08543387253647b86459addbffb16f63f"}, +] + +[package.dependencies] +cryptography = ">=43,<45" + +[package.extras] +dev = ["maturin (>=1.7,<2.0)", "rfc3161-client[doc,lint,test]"] +lint = ["interrogate", "ruff (>=0.7,<0.12)"] +test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] + [[package]] name = "rich" -version = "13.9.4" +version = "14.0.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["main"] files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, + {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, + {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, ] [package.dependencies] @@ -3374,32 +3421,32 @@ files = [ [[package]] name = "s3transfer" -version = "0.11.2" +version = "0.11.4" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "s3transfer-0.11.2-py3-none-any.whl", hash = "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc"}, - {file = "s3transfer-0.11.2.tar.gz", hash = "sha256:3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f"}, + {file = "s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d"}, + {file = "s3transfer-0.11.4.tar.gz", hash = "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679"}, ] [package.dependencies] -botocore = ">=1.36.0,<2.0a.0" +botocore = ">=1.37.4,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.36.0,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] [[package]] name = "selenium" -version = "4.29.0" +version = "4.30.0" description = "Official Python bindings for Selenium WebDriver" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "selenium-4.29.0-py3-none-any.whl", hash = "sha256:ce5d26f1ddc1111641113653af33694c13947dd36c2df09cdd33f554351d372e"}, - {file = "selenium-4.29.0.tar.gz", hash = "sha256:3a62f7ec33e669364a6c0562a701deb69745b569c50d55f1a912bf8eb33358ba"}, + {file = "selenium-4.30.0-py3-none-any.whl", hash = "sha256:90bcd3be86a1762100a093b33e5e4530b328226da94208caadb15ce13243dffd"}, + {file = "selenium-4.30.0.tar.gz", hash = "sha256:16ab890fc7cb21a01e1b1e9a0fbaa9445fe30837eabc66e90b3bacf12138126a"}, ] [package.dependencies] @@ -3412,14 +3459,14 @@ websocket-client = ">=1.8,<2.0" [[package]] name = "setuptools" -version = "75.8.0" +version = "75.9.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" groups = ["worker"] files = [ - {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, - {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, + {file = "setuptools-75.9.1-py3-none-any.whl", hash = "sha256:0a6f876d62f4d978ca1a11ab4daf728d1357731f978543ff18ecdbf9fd071f73"}, + {file = "setuptools-75.9.1.tar.gz", hash = "sha256:b6eca2c3070cdc82f71b4cb4bb2946bc0760a210d11362278cf1ff394e6ea32c"}, ] [package.extras] @@ -3481,81 +3528,81 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.38" +version = "2.0.40" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-win32.whl", hash = "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-win_amd64.whl", hash = "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-win32.whl", hash = "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-win_amd64.whl", hash = "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-win32.whl", hash = "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-win_amd64.whl", hash = "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-win32.whl", hash = "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-win_amd64.whl", hash = "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-win32.whl", hash = "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-win_amd64.whl", hash = "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1"}, - {file = "SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753"}, - {file = "sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ae9597cab738e7cc823f04a704fb754a9249f0b6695a6aeb63b74055cd417a96"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a5c21ab099a83d669ebb251fddf8f5cee4d75ea40a5a1653d9c43d60e20867"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bece9527f5a98466d67fb5d34dc560c4da964240d8b09024bb21c1246545e04e"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:8bb131ffd2165fae48162c7bbd0d97c84ab961deea9b8bab16366543deeab625"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9408fd453d5f8990405cc9def9af46bfbe3183e6110401b407c2d073c3388f47"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-win32.whl", hash = "sha256:00a494ea6f42a44c326477b5bee4e0fc75f6a80c01570a32b57e89cf0fbef85a"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-win_amd64.whl", hash = "sha256:c7b927155112ac858357ccf9d255dd8c044fd9ad2dc6ce4c4149527c901fa4c3"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1ea21bef99c703f44444ad29c2c1b6bd55d202750b6de8e06a955380f4725d7"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:afe63b208153f3a7a2d1a5b9df452b0673082588933e54e7c8aac457cf35e758"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8aae085ea549a1eddbc9298b113cffb75e514eadbb542133dd2b99b5fb3b6af"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ea9181284754d37db15156eb7be09c86e16e50fbe77610e9e7bee09291771a1"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5434223b795be5c5ef8244e5ac98056e290d3a99bdcc539b916e282b160dda00"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15d08d5ef1b779af6a0909b97be6c1fd4298057504eb6461be88bd1696cb438e"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-win32.whl", hash = "sha256:cd2f75598ae70bcfca9117d9e51a3b06fe29edd972fdd7fd57cc97b4dbf3b08a"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-win_amd64.whl", hash = "sha256:2cbafc8d39ff1abdfdda96435f38fab141892dc759a2165947d1a8fffa7ef596"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6bacab7514de6146a1976bc56e1545bee247242fab030b89e5f70336fc0003e"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5654d1ac34e922b6c5711631f2da497d3a7bffd6f9f87ac23b35feea56098011"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35904d63412db21088739510216e9349e335f142ce4a04b69e2528020ee19ed4"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7a80ed86d6aaacb8160a1caef6680d4ddd03c944d985aecee940d168c411d1"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:519624685a51525ddaa7d8ba8265a1540442a2ec71476f0e75241eb8263d6f51"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5f9999a5b0e9689bed96e60ee53c3384f1a05c2dd8068cc2e8361b0df5b7a"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-win32.whl", hash = "sha256:c0cae71e20e3c02c52f6b9e9722bca70e4a90a466d59477822739dc31ac18b4b"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-win_amd64.whl", hash = "sha256:574aea2c54d8f1dd1699449f332c7d9b71c339e04ae50163a3eb5ce4c4325ee4"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d3b31d0a1c44b74d3ae27a3de422dfccd2b8f0b75e51ecb2faa2bf65ab1ba0d"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f7a0f506cf78c80450ed1e816978643d3969f99c4ac6b01104a6fe95c5490a"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bb933a650323e476a2e4fbef8997a10d0003d4da996aad3fd7873e962fdde4d"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959738971b4745eea16f818a2cd086fb35081383b078272c35ece2b07012716"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:110179728e442dae85dd39591beb74072ae4ad55a44eda2acc6ec98ead80d5f2"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8040680eaacdce4d635f12c55c714f3d4c7f57da2bc47a01229d115bd319191"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-win32.whl", hash = "sha256:650490653b110905c10adac69408380688cefc1f536a137d0d69aca1069dc1d1"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-win_amd64.whl", hash = "sha256:2be94d75ee06548d2fc591a3513422b873490efb124048f50556369a834853b0"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50f5885bbed261fc97e2e66c5156244f9704083a674b8d17f24c72217d29baf5"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf0e99cdb600eabcd1d65cdba0d3c91418fee21c4aa1d28db47d095b1064a7d8"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe147fcd85aaed53ce90645c91ed5fca0cc88a797314c70dfd9d35925bd5d106"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf7cee56bd552385c1ee39af360772fbfc2f43be005c78d1140204ad6148438"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4aeb939bcac234b88e2d25d5381655e8353fe06b4e50b1c55ecffe56951d18c2"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c268b5100cfeaa222c40f55e169d484efa1384b44bf9ca415eae6d556f02cb08"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-win32.whl", hash = "sha256:46628ebcec4f23a1584fb52f2abe12ddb00f3bb3b7b337618b80fc1b51177aff"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-win_amd64.whl", hash = "sha256:7e0505719939e52a7b0c65d20e84a6044eb3712bb6f239c6b1db77ba8e173a37"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c884de19528e0fcd9dc34ee94c810581dd6e74aef75437ff17e696c2bfefae3e"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1abb387710283fc5983d8a1209d9696a4eae9db8d7ac94b402981fe2fe2e39ad"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cfa124eda500ba4b0d3afc3e91ea27ed4754e727c7f025f293a22f512bcd4c9"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b6b28d303b9d57c17a5164eb1fd2d5119bb6ff4413d5894e74873280483eeb5"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b5a5bbe29c10c5bfd63893747a1bf6f8049df607638c786252cb9243b86b6706"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f0fda83e113bb0fb27dc003685f32a5dcb99c9c4f41f4fa0838ac35265c23b5c"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-win32.whl", hash = "sha256:957f8d85d5e834397ef78a6109550aeb0d27a53b5032f7a57f2451e1adc37e98"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-win_amd64.whl", hash = "sha256:1ffdf9c91428e59744f8e6f98190516f8e1d05eec90e936eb08b257332c5e870"}, + {file = "sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a"}, + {file = "sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (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\")"} +greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (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\")"} typing-extensions = ">=4.6.0" [package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] @@ -3566,7 +3613,7 @@ mysql-connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] postgresql-pg8000 = ["pg8000 (>=1.29.1)"] postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] @@ -3577,14 +3624,14 @@ sqlcipher = ["sqlcipher3_binary"] [[package]] name = "starlette" -version = "0.45.3" +version = "0.46.1" description = "The little ASGI library that shines." optional = false python-versions = ">=3.9" groups = ["web"] files = [ - {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, - {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, + {file = "starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227"}, + {file = "starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230"}, ] [package.dependencies] @@ -3712,14 +3759,14 @@ sortedcontainers = "*" [[package]] name = "trio-websocket" -version = "0.12.1" +version = "0.12.2" description = "WebSocket library for Trio" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "trio_websocket-0.12.1-py3-none-any.whl", hash = "sha256:608ec746bb287e5d5a66baf483e41194193c5cf05ffaad6240e7d1fcd80d1e6f"}, - {file = "trio_websocket-0.12.1.tar.gz", hash = "sha256:d55ccd4d3eae27c494f3fdae14823317839bdcb8214d1173eacc4d42c69fc91b"}, + {file = "trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"}, + {file = "trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae"}, ] [package.dependencies] @@ -3728,36 +3775,16 @@ outcome = ">=1.2.0" trio = ">=0.11" wsproto = ">=0.14" -[[package]] -name = "tsp-client" -version = "0.2.0" -description = "An IETF Time-Stamp Protocol (TSP) (RFC 3161) client" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "tsp-client-0.2.0.tar.gz", hash = "sha256:6e66148dd116322eb44a7484e5ad33bbe640b997343c443de9cc70fc5eb19987"}, - {file = "tsp_client-0.2.0-py3-none-any.whl", hash = "sha256:0b790d10a68d66782c13f1d7cc7f5206df26b49826c1da80944b7c05b1731784"}, -] - -[package.dependencies] -asn1crypto = ">=0.24.0" -pyOpenSSL = ">=20.0.0" -requests = ">=2.18.4" - -[package.extras] -tests = ["build", "coverage", "mypy", "ruff", "wheel"] - [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main", "dev", "web"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5"}, + {file = "typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b"}, ] [[package]] @@ -3776,28 +3803,44 @@ files = [ mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" +[[package]] +name = "typing-inspection" +version = "0.4.0" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main", "web"] +files = [ + {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, + {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "tzdata" -version = "2025.1" +version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ - {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, - {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] [[package]] name = "tzlocal" -version = "5.3" +version = "5.3.1" description = "tzinfo object for the local timezone" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c"}, - {file = "tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2"}, + {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, + {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, ] [package.dependencies] @@ -3873,14 +3916,14 @@ files = [ [[package]] name = "virtualenv" -version = "20.29.2" +version = "20.30.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, - {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, + {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, + {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, ] [package.dependencies] @@ -3890,7 +3933,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "vk-api" @@ -4032,81 +4075,81 @@ test = ["websockets"] [[package]] name = "websockets" -version = "15.0" +version = "15.0.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "websockets-15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5e6ee18a53dd5743e6155b8ff7e8e477c25b29b440f87f65be8165275c87fef0"}, - {file = "websockets-15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ee06405ea2e67366a661ed313e14cf2a86e84142a3462852eb96348f7219cee3"}, - {file = "websockets-15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8711682a629bbcaf492f5e0af72d378e976ea1d127a2d47584fa1c2c080b436b"}, - {file = "websockets-15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94c4a9b01eede952442c088d415861b0cf2053cbd696b863f6d5022d4e4e2453"}, - {file = "websockets-15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45535fead66e873f411c1d3cf0d3e175e66f4dd83c4f59d707d5b3e4c56541c4"}, - {file = "websockets-15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e389efe46ccb25a1f93d08c7a74e8123a2517f7b7458f043bd7529d1a63ffeb"}, - {file = "websockets-15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:67a04754d121ea5ca39ddedc3f77071651fb5b0bc6b973c71c515415b44ed9c5"}, - {file = "websockets-15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bd66b4865c8b853b8cca7379afb692fc7f52cf898786537dfb5e5e2d64f0a47f"}, - {file = "websockets-15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a4cc73a6ae0a6751b76e69cece9d0311f054da9b22df6a12f2c53111735657c8"}, - {file = "websockets-15.0-cp310-cp310-win32.whl", hash = "sha256:89da58e4005e153b03fe8b8794330e3f6a9774ee9e1c3bd5bc52eb098c3b0c4f"}, - {file = "websockets-15.0-cp310-cp310-win_amd64.whl", hash = "sha256:4ff380aabd7a74a42a760ee76c68826a8f417ceb6ea415bd574a035a111fd133"}, - {file = "websockets-15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dd24c4d256558429aeeb8d6c24ebad4e982ac52c50bc3670ae8646c181263965"}, - {file = "websockets-15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f83eca8cbfd168e424dfa3b3b5c955d6c281e8fc09feb9d870886ff8d03683c7"}, - {file = "websockets-15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4095a1f2093002c2208becf6f9a178b336b7572512ee0a1179731acb7788e8ad"}, - {file = "websockets-15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb915101dfbf318486364ce85662bb7b020840f68138014972c08331458d41f3"}, - {file = "websockets-15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45d464622314973d78f364689d5dbb9144e559f93dca11b11af3f2480b5034e1"}, - {file = "websockets-15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace960769d60037ca9625b4c578a6f28a14301bd2a1ff13bb00e824ac9f73e55"}, - {file = "websockets-15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7cd4b1015d2f60dfe539ee6c95bc968d5d5fad92ab01bb5501a77393da4f596"}, - {file = "websockets-15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f7290295794b5dec470867c7baa4a14182b9732603fd0caf2a5bf1dc3ccabf3"}, - {file = "websockets-15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3abd670ca7ce230d5a624fd3d55e055215d8d9b723adee0a348352f5d8d12ff4"}, - {file = "websockets-15.0-cp311-cp311-win32.whl", hash = "sha256:110a847085246ab8d4d119632145224d6b49e406c64f1bbeed45c6f05097b680"}, - {file = "websockets-15.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7bbbe2cd6ed80aceef2a14e9f1c1b61683194c216472ed5ff33b700e784e37"}, - {file = "websockets-15.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cccc18077acd34c8072578394ec79563664b1c205f7a86a62e94fafc7b59001f"}, - {file = "websockets-15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4c22992e24f12de340ca5f824121a5b3e1a37ad4360b4e1aaf15e9d1c42582d"}, - {file = "websockets-15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1206432cc6c644f6fc03374b264c5ff805d980311563202ed7fef91a38906276"}, - {file = "websockets-15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3cc75ef3e17490042c47e0523aee1bcc4eacd2482796107fd59dd1100a44bc"}, - {file = "websockets-15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b89504227a5311610e4be16071465885a0a3d6b0e82e305ef46d9b064ce5fb72"}, - {file = "websockets-15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56e3efe356416bc67a8e093607315951d76910f03d2b3ad49c4ade9207bf710d"}, - {file = "websockets-15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f2205cdb444a42a7919690238fb5979a05439b9dbb73dd47c863d39640d85ab"}, - {file = "websockets-15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aea01f40995fa0945c020228ab919b8dfc93fc8a9f2d3d705ab5b793f32d9e99"}, - {file = "websockets-15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9f8e33747b1332db11cf7fcf4a9512bef9748cb5eb4d3f7fbc8c30d75dc6ffc"}, - {file = "websockets-15.0-cp312-cp312-win32.whl", hash = "sha256:32e02a2d83f4954aa8c17e03fe8ec6962432c39aca4be7e8ee346b05a3476904"}, - {file = "websockets-15.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc02b159b65c05f2ed9ec176b715b66918a674bd4daed48a9a7a590dd4be1aa"}, - {file = "websockets-15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d2244d8ab24374bed366f9ff206e2619345f9cd7fe79aad5225f53faac28b6b1"}, - {file = "websockets-15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a302241fbe825a3e4fe07666a2ab513edfdc6d43ce24b79691b45115273b5e7"}, - {file = "websockets-15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:10552fed076757a70ba2c18edcbc601c7637b30cdfe8c24b65171e824c7d6081"}, - {file = "websockets-15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c53f97032b87a406044a1c33d1e9290cc38b117a8062e8a8b285175d7e2f99c9"}, - {file = "websockets-15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1caf951110ca757b8ad9c4974f5cac7b8413004d2f29707e4d03a65d54cedf2b"}, - {file = "websockets-15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf1ab71f9f23b0a1d52ec1682a3907e0c208c12fef9c3e99d2b80166b17905f"}, - {file = "websockets-15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bfcd3acc1a81f106abac6afd42327d2cf1e77ec905ae11dc1d9142a006a496b6"}, - {file = "websockets-15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8c5c8e1bac05ef3c23722e591ef4f688f528235e2480f157a9cfe0a19081375"}, - {file = "websockets-15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:86bfb52a9cfbcc09aba2b71388b0a20ea5c52b6517c0b2e316222435a8cdab72"}, - {file = "websockets-15.0-cp313-cp313-win32.whl", hash = "sha256:26ba70fed190708551c19a360f9d7eca8e8c0f615d19a574292b7229e0ae324c"}, - {file = "websockets-15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8"}, - {file = "websockets-15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c348abc5924caa02a62896300e32ea80a81521f91d6db2e853e6b1994017c9f6"}, - {file = "websockets-15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5294fcb410ed0a45d5d1cdedc4e51a60aab5b2b3193999028ea94afc2f554b05"}, - {file = "websockets-15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c24ba103ecf45861e2e1f933d40b2d93f5d52d8228870c3e7bf1299cd1cb8ff1"}, - {file = "websockets-15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc8821a03bcfb36e4e4705316f6b66af28450357af8a575dc8f4b09bf02a3dee"}, - {file = "websockets-15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc5ae23ada6515f31604f700009e2df90b091b67d463a8401c1d8a37f76c1d7"}, - {file = "websockets-15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ac67b542505186b3bbdaffbc303292e1ee9c8729e5d5df243c1f20f4bb9057e"}, - {file = "websockets-15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c86dc2068f1c5ca2065aca34f257bbf4f78caf566eb230f692ad347da191f0a1"}, - {file = "websockets-15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:30cff3ef329682b6182c01c568f551481774c476722020b8f7d0daacbed07a17"}, - {file = "websockets-15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98dcf978d4c6048965d1762abd534c9d53bae981a035bfe486690ba11f49bbbb"}, - {file = "websockets-15.0-cp39-cp39-win32.whl", hash = "sha256:37d66646f929ae7c22c79bc73ec4074d6db45e6384500ee3e0d476daf55482a9"}, - {file = "websockets-15.0-cp39-cp39-win_amd64.whl", hash = "sha256:24d5333a9b2343330f0f4eb88546e2c32a7f5c280f8dd7d3cc079beb0901781b"}, - {file = "websockets-15.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b499caef4bca9cbd0bd23cd3386f5113ee7378094a3cb613a2fa543260fe9506"}, - {file = "websockets-15.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:17f2854c6bd9ee008c4b270f7010fe2da6c16eac5724a175e75010aacd905b31"}, - {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89f72524033abbfde880ad338fd3c2c16e31ae232323ebdfbc745cbb1b3dcc03"}, - {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1657a9eecb29d7838e3b415458cc494e6d1b194f7ac73a34aa55c6fb6c72d1f3"}, - {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e413352a921f5ad5d66f9e2869b977e88d5103fc528b6deb8423028a2befd842"}, - {file = "websockets-15.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8561c48b0090993e3b2a54db480cab1d23eb2c5735067213bb90f402806339f5"}, - {file = "websockets-15.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:190bc6ef8690cd88232a038d1b15714c258f79653abad62f7048249b09438af3"}, - {file = "websockets-15.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:327adab7671f3726b0ba69be9e865bba23b37a605b585e65895c428f6e47e766"}, - {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd8ef197c87afe0a9009f7a28b5dc613bfc585d329f80b7af404e766aa9e8c7"}, - {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:789c43bf4a10cd067c24c321238e800b8b2716c863ddb2294d2fed886fa5a689"}, - {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7394c0b7d460569c9285fa089a429f58465db930012566c03046f9e3ab0ed181"}, - {file = "websockets-15.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ea4f210422b912ebe58ef0ad33088bc8e5c5ff9655a8822500690abc3b1232d"}, - {file = "websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3"}, - {file = "websockets-15.0.tar.gz", hash = "sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, ] [[package]] @@ -4142,27 +4185,39 @@ h11 = ">=0.9.0,<1" [[package]] name = "yt-dlp" -version = "2025.2.19" +version = "2025.3.31" description = "A feature-rich command-line audio/video downloader" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "yt_dlp-2025.2.19-py3-none-any.whl", hash = "sha256:3ed218eaeece55e9d715afd41abc450dc406ee63bf79355169dfde312d38fdb8"}, - {file = "yt_dlp-2025.2.19.tar.gz", hash = "sha256:f33ca76df2e4db31880f2fe408d44f5058d9f135015b13e50610dfbe78245bea"}, + {file = "yt_dlp-2025.3.31-py3-none-any.whl", hash = "sha256:8ecb3aa218a3bebe431119f513a8972b9b9d992edf67168c00ab92329a03baec"}, + {file = "yt_dlp-2025.3.31.tar.gz", hash = "sha256:1bfe0e660d1a70a09e27b2d58f92e30b1e2e362cc487829f2f824346ae49fb91"}, ] [package.extras] build = ["build", "hatchling", "pip", "setuptools (>=71.0.2)", "wheel"] -curl-cffi = ["curl-cffi (==0.5.10) ; os_name == \"nt\" and implementation_name == \"cpython\"", "curl-cffi (>=0.5.10,!=0.6.*,<0.7.2) ; os_name != \"nt\" and implementation_name == \"cpython\""] +curl-cffi = ["curl-cffi (>=0.5.10,<0.6.dev0 || ==0.10.*) ; implementation_name == \"cpython\""] default = ["brotli ; implementation_name == \"cpython\"", "brotlicffi ; implementation_name != \"cpython\"", "certifi", "mutagen", "pycryptodomex", "requests (>=2.32.2,<3)", "urllib3 (>=1.26.17,<3)", "websockets (>=13.0)"] -dev = ["autopep8 (>=2.0,<3.0)", "pre-commit", "pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)", "ruff (>=0.9.0,<0.10.0)"] +dev = ["autopep8 (>=2.0,<3.0)", "pre-commit", "pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)", "ruff (>=0.11.0,<0.12.0)"] pyinstaller = ["pyinstaller (>=6.11.1)"] secretstorage = ["cffi", "secretstorage"] -static-analysis = ["autopep8 (>=2.0,<3.0)", "ruff (>=0.9.0,<0.10.0)"] +static-analysis = ["autopep8 (>=2.0,<3.0)", "ruff (>=0.11.0,<0.12.0)"] test = ["pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)"] +[[package]] +name = "yt-dlp-get-pot" +version = "0.3.0" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "yt_dlp_get_pot-0.3.0-py3-none-any.whl", hash = "sha256:a49a596a3e3c02cd9ce051192ea3fe8168cf24ece8954bed6aa331a87d86954f"}, + {file = "yt_dlp_get_pot-0.3.0.tar.gz", hash = "sha256:ac9530b9e7b3d667235b9119da475f595d2dc7e6f6bbf98b965011be454e8833"}, +] + [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "7efbba5ed47fe36f6767dd60e4578c1db1ac830193b39d230a1e5e6fb4ccb01c" +content-hash = "4e1754247809ac79cab412fab3900ffa88b6bc02d48fe4971d960896e0b72dda" diff --git a/pyproject.toml b/pyproject.toml index 4ba95f9..7b3044d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ requires-python = ">=3.10,<3.13" dependencies = [ "auto-archiver (>=0.13.1)", - "oscrypto @ git+https://github.com/wbond/oscrypto.git@d5f3437ed24257895ae1edd9e503cfb352e635a8", "celery (>=5.0)", "redis (==3.5.3)", "loguru (>=0.7.3,<0.8.0)", From cb47e7b2f7598872a23b031e41b0ddd85ce28c59 Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:09:35 +0100 Subject: [PATCH 27/40] renamed configs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16ed669..7ebe7a1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ To properly set up the API you need to install `docker` and to have these files, 2. a `user-groups.yaml` to manage user permissions 1. note that all local files referenced in `user-groups.yaml` and any orchestration.yaml files should be relative to the home directory so if your service account is in `secrets/orchestration.yaml` use that path and not just `orchestration.yaml`. 2. go through the example file and configure it according to your needs. -3. you will need to create and reference at least one `secrets/orchestration.yaml` file, you can do so by following the instructions in the [auto-archiver](https://github.com/bellingcat/auto-archiver#installation) that automatically generates one for you. If you use the archive sheets feature you will need to create a `orchestrationsheets-sheets.yaml` file as well that should have the `gsheet_feeder` and `gsheet_db` enabled and configured, the auto-archiver has [extensive documentation](https://auto-archiver.readthedocs.io/en/latest/) on how to set this up. +3. you will need to create and reference at least one `secrets/orchestration.yaml` file, you can do so by following the instructions in the [auto-archiver](https://github.com/bellingcat/auto-archiver#installation) that automatically generates one for you. If you use the archive sheets feature you will need to create a `orchestrationsheets-sheets.yaml` file as well that should have the `gsheet_feeder_db` feeder and database enabled and configured, the auto-archiver has [extensive documentation](https://auto-archiver.readthedocs.io/en/latest/) on how to set this up. Do not commit those files, they are .gitignored by default. We also advise you to keep any sensitive files in the `secrets/` folder which is pinned and gitignored. From cf24f800a170720cabb0c933ecb9e173d2bc0584 Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:13:58 +0100 Subject: [PATCH 28/40] fix orchestrator file presence check based on enabled group permissions --- app/shared/user_groups.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/shared/user_groups.py b/app/shared/user_groups.py index 764cbca..444fd6b 100644 --- a/app/shared/user_groups.py +++ b/app/shared/user_groups.py @@ -94,20 +94,31 @@ class GroupPermissions(BaseModel): class GroupModel(BaseModel): description: str - orchestrator: str - orchestrator_sheet: str + orchestrator: str | None = None + orchestrator_sheet: str | None = None permissions: GroupPermissions @classmethod - @field_validator("orchestrator", "orchestrator_sheet", mode="before") + @field_validator("orchestrator", mode="before") def validate_orchestrator(cls, v): - if not os.path.exists(v): + # orchestrator is only needed if the group has archive_url permission + if cls.permissions.archive_url and not os.path.exists(v): + raise ValueError(f"Orchestrator file not found with this path: {v}") + return v + + @classmethod + @field_validator("orchestrator_sheet", mode="before") + def validate_orchestrator_sheet(cls, v): + # orchestrator_sheet is only needed if the group has archive_sheet permission + if cls.permissions.archive_sheet and not os.path.exists(v): raise ValueError(f"Orchestrator file not found with this path: {v}") return v @computed_field @property def service_account_email(self) -> str: + if self.orchestrator_sheet is None: + return "" if hasattr(self, "_service_account_email"): return self._service_account_email orch = yaml.safe_load(open(self.orchestrator_sheet)) From 8677cc33aaa78c312a10152c2e18506c806e848d Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:14:25 +0100 Subject: [PATCH 29/40] fixes test yaml with AA breaking changes --- app/tests/orchestration.test.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/tests/orchestration.test.yaml b/app/tests/orchestration.test.yaml index ef7ed27..9d2e44b 100644 --- a/app/tests/orchestration.test.yaml +++ b/app/tests/orchestration.test.yaml @@ -1,7 +1,8 @@ steps: - feeder: cli_feeder + feeders: + - cli_feeder archivers: # order matters - - youtubedl_archiver + - generic_extractor enrichers: - hash_enricher @@ -12,7 +13,7 @@ steps: - console_db configurations: - gsheet_feeder: + gsheet_feeder_db: service_account: "app/tests/fake_service_account.json" cli_feeder: urls: From aaf3b3b950cfef5a70db1e3bc9047e40cab1406f Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:14:43 +0100 Subject: [PATCH 30/40] updates worker to use AA 1.0.0 --- app/tests/worker/test_worker_main.py | 2 +- app/worker/main.py | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py index 865d039..d78b9ea 100644 --- a/app/tests/worker/test_worker_main.py +++ b/app/tests/worker/test_worker_main.py @@ -119,7 +119,7 @@ class TestCreateSheetTask: res = create_sheet_task(self.sheet.model_dump_json()) m_args.assert_called_once_with( - "interstellar", True, ["--gsheet_feeder.sheet_id", "123"] + "interstellar", True, ["--gsheet_feeder_db.sheet_id", "123"] ) m_orchestrator.return_value.setup.assert_called_once() m_orchestrator.return_value.feed.assert_called_once() diff --git a/app/worker/main.py b/app/worker/main.py index 7716b6b..9ce377f 100644 --- a/app/worker/main.py +++ b/app/worker/main.py @@ -25,10 +25,7 @@ Redis = get_redis() USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME setup_celery_logger(celery) - -# TODO: these are temporary PATCHES for new aa's functionality -# logger.add("app/worker/worker_log.log", level="DEBUG") -logger.remove = lambda x: print(f"logger.remove({x})") +AA_LOGGER_ID = None # TODO: after release, as it requires updating past entries with sheet_id where tag @@ -41,14 +38,19 @@ logger.remove = lambda x: print(f"logger.remove({x})") retry_kwargs={"max_retries": 1}, ) def create_archive_task(self, archive_json: str): + global AA_LOGGER_ID archive = schemas.ArchiveCreate.model_validate_json(archive_json) # call auto-archiver args = get_orchestrator_args(archive.group_id, False, [archive.url]) + result = None try: orchestrator = ArchivingOrchestrator() + orchestrator.logger_id = AA_LOGGER_ID # ensure single logger orchestrator.setup(args) - result = next(orchestrator.feed()) + AA_LOGGER_ID = orchestrator.logger_id + for orch_res in orchestrator.feed(): + result = orch_res except SystemExit as e: log_error(e, "create_archive_task: SystemExit from AA") except Exception as e: @@ -68,6 +70,7 @@ def create_archive_task(self, archive_json: str): @celery.task(name="create_sheet_task", bind=True) def create_sheet_task(self, sheet_json: str): + global AA_LOGGER_ID sheet = schemas.SubmitSheet.model_validate_json(sheet_json) queue_name = (create_sheet_task.request.delivery_info or {}).get( "routing_key", "unknown" @@ -75,10 +78,12 @@ def create_sheet_task(self, sheet_json: str): logger.info(f"[queue={queue_name}] SHEET START {sheet=}") args = get_orchestrator_args( - sheet.group_id, True, ["--gsheet_feeder.sheet_id", sheet.sheet_id] + sheet.group_id, True, ["--gsheet_feeder_db.sheet_id", sheet.sheet_id] ) orchestrator = ArchivingOrchestrator() + orchestrator.logger_id = AA_LOGGER_ID # ensure single logger orchestrator.setup(args) + AA_LOGGER_ID = orchestrator.logger_id stats = {"archived": 0, "failed": 0, "errors": []} try: @@ -128,8 +133,7 @@ def create_sheet_task(self, sheet_json: str): def get_orchestrator_args( group_id: str, orchestrator_for_sheet: bool, cli_args: list = None ) -> list: - if cli_args is None: - cli_args = [] + cli_args.append("--logging.enabled=false") aa_configs = [] with get_db() as session: From 42a8751c86c0d314ceec7941dcc0c59506dce55f Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:15:48 +0100 Subject: [PATCH 31/40] removes recurring log --- app/web/security.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/web/security.py b/app/web/security.py index ffdeee6..5b73997 100644 --- a/app/web/security.py +++ b/app/web/security.py @@ -84,8 +84,9 @@ def authenticate_user(access_token) -> (bool, str): if FIREBASE_OAUTH_ENABLED: try: return firebase_login_attempt(access_token) - except exceptions.FirebaseError as e: - logger.warning(f"Error verifying ID token: {str(e)[:80]}...") + except exceptions.FirebaseError: + # used a non-Firebase token, fallback to Google OAuth + pass # https://cloud.google.com/docs/authentication/token-types#access if not isinstance(access_token, str) or len(access_token) < 10: From 029152e88762c9ffa24e74a848ac10cd80ecd33d Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:16:30 +0100 Subject: [PATCH 32/40] version bump --- app/web/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/web/config.py b/app/web/config.py index da25dcb..9f2e0fd 100644 --- a/app/web/config.py +++ b/app/web/config.py @@ -1,4 +1,4 @@ -VERSION = "0.9.4" +VERSION = "0.10.0" API_DESCRIPTION = """ #### API for the Auto-Archiver project, a tool to archive web pages and Google Sheets. From aae5db2fa892e2d6d3082fe1ace9fac279503eca Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:09:25 +0100 Subject: [PATCH 33/40] updates base dependencies --- poetry.lock | 1357 +++++++++++++++++++++++++----------------------- pyproject.toml | 1 - 2 files changed, 706 insertions(+), 652 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3d4c1ef..d090800 100644 --- a/poetry.lock +++ b/poetry.lock @@ -37,23 +37,23 @@ docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.1)"] [[package]] name = "alembic" -version = "1.14.1" +version = "1.15.2" description = "A database migration tool for SQLAlchemy." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["web"] files = [ - {file = "alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5"}, - {file = "alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"}, + {file = "alembic-1.15.2-py3-none-any.whl", hash = "sha256:2e76bd916d547f6900ec4bb5a90aeac1485d2c92536923d0b138c02b126edc53"}, + {file = "alembic-1.15.2.tar.gz", hash = "sha256:1c72391bbdeffccfe317eefba686cb9a3c078005478885413b95c3b26c57a8a7"}, ] [package.dependencies] Mako = "*" -SQLAlchemy = ">=1.3.0" -typing-extensions = ">=4" +SQLAlchemy = ">=1.4.0" +typing-extensions = ">=4.12" [package.extras] -tz = ["backports.zoneinfo ; python_version < \"3.9\"", "tzdata"] +tz = ["tzdata"] [[package]] name = "amqp" @@ -84,14 +84,14 @@ files = [ [[package]] name = "anyio" -version = "4.8.0" +version = "4.9.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" groups = ["dev", "web"] files = [ - {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, - {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, + {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, + {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, ] [package.dependencies] @@ -101,52 +101,40 @@ sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "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)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] -[[package]] -name = "asn1crypto" -version = "1.5.1" -description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, - {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, -] - [[package]] name = "attrs" -version = "25.1.0" +version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, - {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "authlib" -version = "1.4.1" +version = "1.5.2" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "Authlib-1.4.1-py2.py3-none-any.whl", hash = "sha256:edc29c3f6a3e72cd9e9f45fff67fc663a2c364022eb0371c003f22d5405915c1"}, - {file = "authlib-1.4.1.tar.gz", hash = "sha256:30ead9ea4993cdbab821dc6e01e818362f92da290c04c7f6a1940f86507a790d"}, + {file = "authlib-1.5.2-py2.py3-none-any.whl", hash = "sha256:8804dd4402ac5e4a0435ac49e0b6e19e395357cfa632a3f624dcb4f6df13b4b1"}, + {file = "authlib-1.5.2.tar.gz", hash = "sha256:fe85ec7e50c5f86f1e2603518bb3b4f632985eb4a355e52256530790e326c512"}, ] [package.dependencies] @@ -154,22 +142,22 @@ cryptography = "*" [[package]] name = "auto-archiver" -version = "0.13.4" +version = "1.0.0" description = "Automatically archive links to videos, images, and social media content from Google Sheets (and more)." optional = false python-versions = "<3.13,>=3.10" groups = ["main"] files = [ - {file = "auto_archiver-0.13.4-py3-none-any.whl", hash = "sha256:490ee0dbc86e3481ee06cdbfbbaf397cbc9733b4aaac8cac233f29af5dc4ba53"}, - {file = "auto_archiver-0.13.4.tar.gz", hash = "sha256:dac206f643e8101bb1efdea2e6cbdfaca1e3ae50cfe3fa34b466b7518337d675"}, + {file = "auto_archiver-1.0.0-py3-none-any.whl", hash = "sha256:b4c20a3cff9a4e98815c858b662b10cd0afc7c80bad1e4adacbd33cfafc97855"}, + {file = "auto_archiver-1.0.0.tar.gz", hash = "sha256:2ce5b40c87bd67661357154ee5f06957b956396bc39f293c370e8237e23bae4e"}, ] [package.dependencies] beautifulsoup4 = ">=0.0.0" +bgutil-ytdlp-pot-provider = ">=0.7.3,<0.8.0" boto3 = ">=1.28.0,<2.0.0" bs4 = ">=0.0.0" -certvalidator = ">=0.0.0" -cryptography = ">=41.0.0,<42.0.0" +cryptography = ">44.0.1,<45.0.0" dataclasses-json = ">=0.0.0" dateparser = ">=0.0.0" ffmpeg-python = ">=0.0.0" @@ -183,23 +171,23 @@ jsonlines = ">=0.0.0" loguru = ">=0.0.0" numpy = "2.1.3" oauth2client = ">=0.0.0" +opentimestamps = ">=0.4.5,<0.5.0" pdqhash = ">=0.0.0" pillow = ">=0.0.0" -pyOpenSSL = "24.2.1" pysubs2 = ">=0.0.0" python-slugify = ">=0.0.0" python-twitter-v2 = ">=0.0.0" requests = {version = ">=0.0.0", extras = ["socks"]} retrying = ">=0.0.0" +rfc3161-client = ">=1.0.1,<2.0.0" rich-argparse = ">=1.6.0,<2.0.0" ruamel-yaml = ">=0.18.10,<0.19.0" selenium = ">=0.0.0" telethon = ">=0.0.0" tqdm = ">=0.0.0" -tsp-client = ">=0.0.0" vk-url-scraper = ">=0.0.0" warcio = ">=0.0.0" -yt-dlp = ">=2025.1.26,<2026.0.0" +yt-dlp = ">=2025.3.21,<2026.0.0" [[package]] name = "beautifulsoup4" @@ -224,6 +212,21 @@ charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "bgutil-ytdlp-pot-provider" +version = "0.7.4" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bgutil_ytdlp_pot_provider-0.7.4-py3-none-any.whl", hash = "sha256:5f0b1d884fec66dff703c421ea06f5fc9b11022d9c0babdaa0cab13ed99b9d77"}, + {file = "bgutil_ytdlp_pot_provider-0.7.4.tar.gz", hash = "sha256:b6c1462b8f979540078085cd82462ef967b8b70cd0810d469243a31f5081e5c6"}, +] + +[package.dependencies] +yt-dlp-get-pot = ">=0.1.1" + [[package]] name = "billiard" version = "4.2.1" @@ -250,18 +253,18 @@ files = [ [[package]] name = "boto3" -version = "1.36.26" +version = "1.37.25" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "boto3-1.36.26-py3-none-any.whl", hash = "sha256:f67d014a7c5a3cd540606d64d7cb9eec3600cf42acab1ac0518df9751ae115e2"}, - {file = "boto3-1.36.26.tar.gz", hash = "sha256:523b69457eee55ac15aa707c0e768b2a45ca1521f95b2442931090633ec72458"}, + {file = "boto3-1.37.25-py3-none-any.whl", hash = "sha256:00a025c621198508dc20c45224baaa7bd2a695323d999cce08b0d4deab5ada6f"}, + {file = "boto3-1.37.25.tar.gz", hash = "sha256:23e9cbad028ef3723567f4556411ee8d0f732594316b4c78c174a03ba3ca3159"}, ] [package.dependencies] -botocore = ">=1.36.26,<1.37.0" +botocore = ">=1.37.25,<1.38.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.11.0,<0.12.0" @@ -270,14 +273,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.36.26" +version = "1.37.25" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "botocore-1.36.26-py3-none-any.whl", hash = "sha256:4e3f19913887a58502e71ef8d696fe7eaa54de7813ff73390cd5883f837dfa6e"}, - {file = "botocore-1.36.26.tar.gz", hash = "sha256:4a63bcef7ecf6146fd3a61dc4f9b33b7473b49bdaf1770e9aaca6eee0c9eab62"}, + {file = "botocore-1.37.25-py3-none-any.whl", hash = "sha256:e35f10df0c3bcf42f4680439148462073fe6445d8938679f0576eb189fb034d7"}, + {file = "botocore-1.37.25.tar.gz", hash = "sha256:6f8cefd769df170809816d66bde2e12c43f557ca6cf18c807922003319b52991"}, ] [package.dependencies] @@ -474,60 +477,60 @@ files = [ [[package]] name = "celery" -version = "5.4.0" +version = "5.5.0" description = "Distributed Task Queue." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, - {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, + {file = "celery-5.5.0-py3-none-any.whl", hash = "sha256:f4170c6e5952281318448a899d9e9a15b9cbd007e002091766900dc8f71b9394"}, + {file = "celery-5.5.0.tar.gz", hash = "sha256:10d49f9926d16237310109b0e6e1e2f7a2133b84e684bb36534d7663e66919bb"}, ] [package.dependencies] -billiard = ">=4.2.0,<5.0" +billiard = ">=4.2.1,<5.0" click = ">=8.1.2,<9.0" click-didyoumean = ">=0.3.0" click-plugins = ">=1.1.1" click-repl = ">=0.2.0" -kombu = ">=5.3.4,<6.0" +kombu = ">=5.5.2,<5.6" python-dateutil = ">=2.8.2" -tzdata = ">=2022.7" vine = ">=5.1.0,<6.0" [package.extras] arangodb = ["pyArango (>=2.0.2)"] -auth = ["cryptography (==42.0.5)"] -azureblockblob = ["azure-storage-blob (>=12.15.0)"] +auth = ["cryptography (==44.0.2)"] +azureblockblob = ["azure-identity (>=1.19.0)", "azure-storage-blob (>=12.15.0)"] brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] cassandra = ["cassandra-driver (>=3.25.0,<4)"] consul = ["python-consul2 (==0.1.5)"] cosmosdbsql = ["pydocumentdb (==2.3.5)"] couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] -couchdb = ["pycouchdb (==1.14.2)"] +couchdb = ["pycouchdb (==1.16.0)"] django = ["Django (>=2.2.28)"] dynamodb = ["boto3 (>=1.26.143)"] -elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] +elasticsearch = ["elastic-transport (<=8.17.1)", "elasticsearch (<=8.17.2)"] eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] -gcs = ["google-cloud-storage (>=2.10.0)"] +gcs = ["google-cloud-firestore (==2.20.1)", "google-cloud-storage (>=2.10.0)", "grpcio (==1.67.0)"] gevent = ["gevent (>=1.5.0)"] librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] -mongodb = ["pymongo[srv] (>=4.0.2)"] -msgpack = ["msgpack (==1.0.8)"] +mongodb = ["pymongo (==4.10.1)"] +msgpack = ["msgpack (==1.1.0)"] +pydantic = ["pydantic (>=2.4)"] pymemcache = ["python-memcached (>=1.61)"] pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] -pytest = ["pytest-celery[all] (>=1.0.0)"] +pytest = ["pytest-celery[all] (>=1.2.0,<1.3.0)"] redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] s3 = ["boto3 (>=1.26.143)"] -slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem (==4.1.5) ; platform_python_implementation != \"PyPy\""] +slmq = ["softlayer_messaging (>=1.0.3)"] +solar = ["ephem (==4.2) ; platform_python_implementation != \"PyPy\""] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "urllib3 (>=1.26.16)"] tblib = ["tblib (>=1.3.0) ; python_version < \"3.8.0\"", "tblib (>=1.5.0) ; python_version >= \"3.8.0\""] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=1.3.1)"] -zstd = ["zstandard (==0.22.0)"] +zstd = ["zstandard (==0.23.0)"] [[package]] name = "certifi" @@ -541,22 +544,6 @@ files = [ {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] -[[package]] -name = "certvalidator" -version = "0.11.1" -description = "Validates X.509 certificates and paths" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "certvalidator-0.11.1-py2.py3-none-any.whl", hash = "sha256:77520b269f516d4fb0902998d5bd0eb3727fe153b659aa1cb828dcf12ea6b8de"}, - {file = "certvalidator-0.11.1.tar.gz", hash = "sha256:922d141c94393ab285ca34338e18dd4093e3ae330b1f278e96c837cb62cffaad"}, -] - -[package.dependencies] -asn1crypto = ">=0.18.1" -oscrypto = ">=0.16.1" - [[package]] name = "cffi" version = "1.17.1" @@ -633,6 +620,7 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] +markers = {main = "os_name == \"nt\" and implementation_name != \"pypy\" or platform_python_implementation != \"PyPy\"", web = "platform_python_implementation != \"PyPy\""} [package.dependencies] pycparser = "*" @@ -833,75 +821,75 @@ markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", [[package]] name = "coverage" -version = "7.6.12" +version = "7.8.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, - {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, - {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, - {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, - {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, - {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, - {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, - {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, - {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, - {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, - {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, - {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, - {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, - {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, - {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, - {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, - {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, - {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, - {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, - {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, - {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, - {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, - {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, - {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, - {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, - {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, - {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, - {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, - {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, - {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, - {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, - {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, - {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, - {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, ] [package.extras] @@ -909,48 +897,60 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "cryptography" -version = "41.0.7" +version = "44.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.7" +python-versions = "!=3.9.0,!=3.9.1,>=3.7" groups = ["main", "web"] files = [ - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, - {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, - {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, - {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, + {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"}, + {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"}, + {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"}, + {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"}, + {file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"}, + {file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"}, + {file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"}, + {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"}, + {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"}, + {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"}, + {file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"}, + {file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"}, + {file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"}, + {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"}, + {file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"}, ] [package.dependencies] -cffi = ">=1.12" +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -nox = ["nox"] -pep8test = ["black", "check-sdist", "mypy", "ruff"] -sdist = ["build"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] +pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -1059,19 +1059,19 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.115.8" +version = "0.115.12" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" groups = ["web"] files = [ - {file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"}, - {file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"}, + {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, + {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, ] [package.dependencies] pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.46.0" +starlette = ">=0.40.0,<0.47.0" typing-extensions = ">=4.8.0" [package.extras] @@ -1144,14 +1144,14 @@ dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4 [[package]] name = "filelock" -version = "3.17.0" +version = "3.18.0" description = "A platform independent file lock." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, - {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, ] [package.extras] @@ -1193,19 +1193,19 @@ files = [ [[package]] name = "google-api-core" -version = "2.24.1" +version = "2.24.2" description = "Google API client core library" optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1"}, - {file = "google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a"}, + {file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"}, + {file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"}, ] [package.dependencies] -google-auth = ">=2.14.1,<3.0.dev0" -googleapis-common-protos = ">=1.56.2,<2.0.dev0" +google-auth = ">=2.14.1,<3.0.0" +googleapis-common-protos = ">=1.56.2,<2.0.0" grpcio = [ {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, {version = ">=1.33.2,<2.0dev", optional = true, markers = "python_version < \"3.11\" and extra == \"grpc\""}, @@ -1214,9 +1214,9 @@ grpcio-status = [ {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, ] -proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" -requests = ">=2.18.0,<3.0.0.dev0" +proto-plus = ">=1.22.3,<2.0.0" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" +requests = ">=2.18.0,<3.0.0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] @@ -1226,21 +1226,21 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.161.0" +version = "2.166.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "google_api_python_client-2.161.0-py2.py3-none-any.whl", hash = "sha256:9476a5a4f200bae368140453df40f9cda36be53fa7d0e9a9aac4cdb859a26448"}, - {file = "google_api_python_client-2.161.0.tar.gz", hash = "sha256:324c0cce73e9ea0a0d2afd5937e01b7c2d6a4d7e2579cdb6c384f9699d6c9f37"}, + {file = "google_api_python_client-2.166.0-py2.py3-none-any.whl", hash = "sha256:dd8cc74d9fc18538ab05cbd2e93cb4f82382f910c5f6945db06c91f1deae6e45"}, + {file = "google_api_python_client-2.166.0.tar.gz", hash = "sha256:b8cf843bd9d736c134aef76cf1dc7a47c9283a2ef24267b97207b9dd43b30ef7"}, ] [package.dependencies] -google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0.dev0" -google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" +google-api-core = ">=1.31.5,<2.0.dev0 || >2.3.0,<3.0.0" +google-auth = ">=1.32.0,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" google-auth-httplib2 = ">=0.2.0,<1.0.0" -httplib2 = ">=0.19.0,<1.dev0" +httplib2 = ">=0.19.0,<1.0.0" uritemplate = ">=3.0.1,<5" [[package]] @@ -1371,39 +1371,46 @@ tracing = ["opentelemetry-api (>=1.1.0)"] [[package]] name = "google-crc32c" -version = "1.6.0" +version = "1.7.1" description = "A python wrapper of the C library 'Google CRC32C'" optional = false python-versions = ">=3.9" groups = ["web"] files = [ - {file = "google_crc32c-1.6.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa"}, - {file = "google_crc32c-1.6.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9"}, - {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7"}, - {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e"}, - {file = "google_crc32c-1.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc"}, - {file = "google_crc32c-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42"}, - {file = "google_crc32c-1.6.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4"}, - {file = "google_crc32c-1.6.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8"}, - {file = "google_crc32c-1.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d"}, - {file = "google_crc32c-1.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f"}, - {file = "google_crc32c-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3"}, - {file = "google_crc32c-1.6.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d"}, - {file = "google_crc32c-1.6.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b"}, - {file = "google_crc32c-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00"}, - {file = "google_crc32c-1.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3"}, - {file = "google_crc32c-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760"}, - {file = "google_crc32c-1.6.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205"}, - {file = "google_crc32c-1.6.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0"}, - {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2"}, - {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871"}, - {file = "google_crc32c-1.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57"}, - {file = "google_crc32c-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c"}, - {file = "google_crc32c-1.6.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc"}, - {file = "google_crc32c-1.6.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d"}, - {file = "google_crc32c-1.6.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24"}, - {file = "google_crc32c-1.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d"}, - {file = "google_crc32c-1.6.0.tar.gz", hash = "sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc"}, + {file = "google_crc32c-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76"}, + {file = "google_crc32c-1.7.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603"}, + {file = "google_crc32c-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a"}, + {file = "google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06"}, + {file = "google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9"}, + {file = "google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77"}, + {file = "google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53"}, + {file = "google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d"}, + {file = "google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194"}, + {file = "google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e"}, + {file = "google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337"}, + {file = "google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65"}, + {file = "google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6"}, + {file = "google_crc32c-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35"}, + {file = "google_crc32c-1.7.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638"}, + {file = "google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb"}, + {file = "google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6"}, + {file = "google_crc32c-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db"}, + {file = "google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3"}, + {file = "google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9"}, + {file = "google_crc32c-1.7.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315"}, + {file = "google_crc32c-1.7.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582"}, + {file = "google_crc32c-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349"}, + {file = "google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589"}, + {file = "google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b"}, + {file = "google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48"}, + {file = "google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82"}, + {file = "google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472"}, ] [package.extras] @@ -1430,21 +1437,21 @@ requests = ["requests (>=2.18.0,<3.0.0dev)"] [[package]] name = "googleapis-common-protos" -version = "1.68.0" +version = "1.69.2" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "googleapis_common_protos-1.68.0-py2.py3-none-any.whl", hash = "sha256:aaf179b2f81df26dfadac95def3b16a95064c76a5f45f07e4c68a21bb371c4ac"}, - {file = "googleapis_common_protos-1.68.0.tar.gz", hash = "sha256:95d38161f4f9af0d9423eed8fb7b64ffd2568c3464eb542ff02c5bfa1953ab3c"}, + {file = "googleapis_common_protos-1.69.2-py3-none-any.whl", hash = "sha256:0b30452ff9c7a27d80bfc5718954063e8ab53dd3697093d3bc99581f5fd24212"}, + {file = "googleapis_common_protos-1.69.2.tar.gz", hash = "sha256:3e1b904a27a33c821b4b749fd31d334c0c9c30e6113023d495e48979a3dc9c5f"}, ] [package.dependencies] -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" [package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] [[package]] name = "greenlet" @@ -1619,14 +1626,14 @@ protobuf = ">=5.26.1,<6.0dev" [[package]] name = "gspread" -version = "6.1.4" +version = "6.2.0" description = "Google Spreadsheets Python API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "gspread-6.1.4-py3-none-any.whl", hash = "sha256:c34781c426031a243ad154952b16f21ac56a5af90687885fbee3d1fba5280dcd"}, - {file = "gspread-6.1.4.tar.gz", hash = "sha256:b8eec27de7cadb338bb1b9f14a9be168372dee8965c0da32121816b5050ac1de"}, + {file = "gspread-6.2.0-py3-none-any.whl", hash = "sha256:7fa1a11e1ecacc6c5946fa016be05941baca8540404314f59aec963dd8ae5db3"}, + {file = "gspread-6.2.0.tar.gz", hash = "sha256:bc3d02d1c39e0b40bfc8035b4fec407aa71a17f343fc81cc7e3f75bfa6555de6"}, ] [package.dependencies] @@ -1709,14 +1716,14 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "identify" -version = "2.6.8" +version = "2.6.9" description = "File identification library for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "identify-2.6.8-py2.py3-none-any.whl", hash = "sha256:83657f0f766a3c8d0eaea16d4ef42494b39b34629a4b3192a9d020d349b3e255"}, - {file = "identify-2.6.8.tar.gz", hash = "sha256:61491417ea2c0c5c670484fd8abbb34de34cdae1e5f39a73ee65e48e4bb663fc"}, + {file = "identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150"}, + {file = "identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf"}, ] [package.extras] @@ -1739,14 +1746,14 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -1769,14 +1776,14 @@ browser-cookie3 = ["browser_cookie3 (>=0.19.1)"] [[package]] name = "jinja2" -version = "3.1.5" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -1814,19 +1821,19 @@ attrs = ">=19.2.0" [[package]] name = "kombu" -version = "5.4.2" +version = "5.5.2" description = "Messaging library for Python." optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763"}, - {file = "kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"}, + {file = "kombu-5.5.2-py3-none-any.whl", hash = "sha256:40f3674ed19603b8a771b6c74de126dbf8879755a0337caac6602faa82d539cd"}, + {file = "kombu-5.5.2.tar.gz", hash = "sha256:2dd27ec84fd843a4e0a7187424313f87514b344812cb98c25daddafbb6a7ff0e"}, ] [package.dependencies] amqp = ">=5.1.1,<6.0.0" -tzdata = {version = "*", markers = "python_version >= \"3.9\""} +tzdata = {version = "2025.2", markers = "python_version >= \"3.9\""} vine = "5.1.0" [package.extras] @@ -1834,15 +1841,16 @@ azureservicebus = ["azure-servicebus (>=7.10.0)"] azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] confluentkafka = ["confluent-kafka (>=2.2.0)"] consul = ["python-consul2 (==0.1.5)"] +gcpubsub = ["google-cloud-monitoring (>=2.16.0)", "google-cloud-pubsub (>=2.18.4)", "grpcio (==1.67.0)", "protobuf (==4.25.5)"] librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] mongodb = ["pymongo (>=4.1.1)"] msgpack = ["msgpack (==1.1.0)"] pyro = ["pyro4 (==4.82)"] qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] -redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] +redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2,<=5.2.1)"] slmq = ["softlayer-messaging (>=1.0.3)"] sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5) ; sys_platform != \"win32\" and platform_python_implementation == \"CPython\"", "urllib3 (>=1.26.16)"] +sqs = ["boto3 (>=1.26.143)", "urllib3 (>=1.26.16)"] yaml = ["PyYAML (>=3.10)"] zookeeper = ["kazoo (>=2.8.0)"] @@ -2225,23 +2233,20 @@ signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] -name = "oscrypto" -version = "1.3.0" -description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD." +name = "opentimestamps" +version = "0.4.5" +description = "Create and verify OpenTimestamps proofs" optional = false python-versions = "*" groups = ["main"] -files = [] -develop = false +files = [ + {file = "opentimestamps-0.4.5-py3-none-any.whl", hash = "sha256:a4912b3bd1b612a3ef5fac925b9137889e6c5cb91cc9e76c8202a2bf8abe26b5"}, + {file = "opentimestamps-0.4.5.tar.gz", hash = "sha256:56726ccde97fb67f336a7f237ce36808e5593c3089d68d900b1c83d0ebf9dcfa"}, +] [package.dependencies] -asn1crypto = ">=1.5.1" - -[package.source] -type = "git" -url = "https://github.com/wbond/oscrypto.git" -reference = "d5f3437ed24257895ae1edd9e503cfb352e635a8" -resolved_reference = "d5f3437ed24257895ae1edd9e503cfb352e635a8" +pycryptodomex = ">=3.3.1" +python-bitcoinlib = ">=0.9.0,<0.13.0" [[package]] name = "outcome" @@ -2382,20 +2387,20 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.7" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" @@ -2415,14 +2420,14 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "4.1.0" +version = "4.2.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, - {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, + {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, + {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, ] [package.dependencies] @@ -2449,14 +2454,14 @@ twisted = ["twisted"] [[package]] name = "prometheus-fastapi-instrumentator" -version = "7.0.2" +version = "7.1.0" description = "Instrument your FastAPI app with Prometheus metrics" optional = false python-versions = ">=3.8" groups = ["web"] files = [ - {file = "prometheus_fastapi_instrumentator-7.0.2-py3-none-any.whl", hash = "sha256:975e39992acb7a112758ff13ba95317e6c54d1bbf605f9156f31ac9f2800c32d"}, - {file = "prometheus_fastapi_instrumentator-7.0.2.tar.gz", hash = "sha256:8a4d8fb13dbe19d2882ac6af9ce236e4e1f98dc48e3fa44fe88d8e23ac3c953f"}, + {file = "prometheus_fastapi_instrumentator-7.1.0-py3-none-any.whl", hash = "sha256:978130f3c0bb7b8ebcc90d35516a6fe13e02d2eb358c8f83887cdef7020c31e9"}, + {file = "prometheus_fastapi_instrumentator-7.1.0.tar.gz", hash = "sha256:be7cd61eeea4e5912aeccb4261c6631b3f227d8924542d79eaf5af3f439cbe5e"}, ] [package.dependencies] @@ -2480,41 +2485,41 @@ wcwidth = "*" [[package]] name = "proto-plus" -version = "1.26.0" +version = "1.26.1" description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"}, - {file = "proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22"}, + {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, + {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, ] [package.dependencies] -protobuf = ">=3.19.0,<6.0.0dev" +protobuf = ">=3.19.0,<7.0.0" [package.extras] testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "5.29.3" +version = "5.29.4" description = "" optional = false python-versions = ">=3.8" groups = ["main", "web"] files = [ - {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, - {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, - {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, - {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, - {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, - {file = "protobuf-5.29.3-cp38-cp38-win32.whl", hash = "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252"}, - {file = "protobuf-5.29.3-cp38-cp38-win_amd64.whl", hash = "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107"}, - {file = "protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7"}, - {file = "protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da"}, - {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, - {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, + {file = "protobuf-5.29.4-cp310-abi3-win32.whl", hash = "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7"}, + {file = "protobuf-5.29.4-cp310-abi3-win_amd64.whl", hash = "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d"}, + {file = "protobuf-5.29.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:307ecba1d852ec237e9ba668e087326a67564ef83e45a0189a772ede9e854dd0"}, + {file = "protobuf-5.29.4-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e"}, + {file = "protobuf-5.29.4-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922"}, + {file = "protobuf-5.29.4-cp38-cp38-win32.whl", hash = "sha256:1832f0515b62d12d8e6ffc078d7e9eb06969aa6dc13c13e1036e39d73bebc2de"}, + {file = "protobuf-5.29.4-cp38-cp38-win_amd64.whl", hash = "sha256:476cb7b14914c780605a8cf62e38c2a85f8caff2e28a6a0bad827ec7d6c85d68"}, + {file = "protobuf-5.29.4-cp39-cp39-win32.whl", hash = "sha256:fd32223020cb25a2cc100366f1dedc904e2d71d9322403224cdde5fdced0dabe"}, + {file = "protobuf-5.29.4-cp39-cp39-win_amd64.whl", hash = "sha256:678974e1e3a9b975b8bc2447fca458db5f93a2fb6b0c8db46b6675b5b5346812"}, + {file = "protobuf-5.29.4-py3-none-any.whl", hash = "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862"}, + {file = "protobuf-5.29.4.tar.gz", hash = "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99"}, ] [[package]] @@ -2571,18 +2576,18 @@ files = [ [[package]] name = "pyasn1-modules" -version = "0.4.1" +version = "0.4.2" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" groups = ["main", "web"] files = [ - {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, - {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.7.0" +pyasn1 = ">=0.6.1,<0.7.0" [[package]] name = "pycparser" @@ -2595,65 +2600,64 @@ files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +markers = {main = "os_name == \"nt\" and implementation_name != \"pypy\" or platform_python_implementation != \"PyPy\"", web = "platform_python_implementation != \"PyPy\""} [[package]] name = "pycryptodomex" -version = "3.21.0" +version = "3.22.0" description = "Cryptographic library for Python" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main"] files = [ - {file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"}, - {file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"}, - {file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"}, - {file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"}, - {file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"}, - {file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"}, - {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"}, - {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"}, - {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:41673e5cc39a8524557a0472077635d981172182c9fe39ce0b5f5c19381ffaff"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:276be1ed006e8fd01bba00d9bd9b60a0151e478033e86ea1cb37447bbc057edc"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:813e57da5ceb4b549bab96fa548781d9a63f49f1d68fdb148eeac846238056b7"}, + {file = "pycryptodomex-3.22.0-cp27-cp27m-win32.whl", hash = "sha256:d7beeacb5394765aa8dabed135389a11ee322d3ee16160d178adc7f8ee3e1f65"}, + {file = "pycryptodomex-3.22.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:b3746dedf74787da43e4a2f85bd78f5ec14d2469eb299ddce22518b3891f16ea"}, + {file = "pycryptodomex-3.22.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5ebc09b7d8964654aaf8a4f5ac325f2b0cc038af9bea12efff0cd4a5bb19aa42"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:aef4590263b9f2f6283469e998574d0bd45c14fb262241c27055b82727426157"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5ac608a6dce9418d4f300fab7ba2f7d499a96b462f2b9b5c90d8d994cd36dcad"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a24f681365ec9757ccd69b85868bbd7216ba451d0f86f6ea0eed75eeb6975db"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:259664c4803a1fa260d5afb322972813c5fe30ea8b43e54b03b7e3a27b30856b"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7127d9de3c7ce20339e06bcd4f16f1a1a77f1471bcf04e3b704306dde101b719"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee75067b35c93cc18b38af47b7c0664998d8815174cfc66dd00ea1e244eb27e6"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:1a8b0c5ba061ace4bcd03496d42702c3927003db805b8ec619ea6506080b381d"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bfe4fe3233ef3e58028a3ad8f28473653b78c6d56e088ea04fe7550c63d4d16b"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-win32.whl", hash = "sha256:2cac9ed5c343bb3d0075db6e797e6112514764d08d667c74cb89b931aac9dddd"}, + {file = "pycryptodomex-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:ff46212fda7ee86ec2f4a64016c994e8ad80f11ef748131753adb67e9b722ebd"}, + {file = "pycryptodomex-3.22.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:5bf3ce9211d2a9877b00b8e524593e2209e370a287b3d5e61a8c45f5198487e2"}, + {file = "pycryptodomex-3.22.0-pp27-pypy_73-win32.whl", hash = "sha256:684cb57812cd243217c3d1e01a720c5844b30f0b7b64bb1a49679f7e1e8a54ac"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c8cffb03f5dee1026e3f892f7cffd79926a538c67c34f8b07c90c0bd5c834e27"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:140b27caa68a36d0501b05eb247bd33afa5f854c1ee04140e38af63c750d4e39"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:644834b1836bb8e1d304afaf794d5ae98a1d637bd6e140c9be7dd192b5374811"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c506aba3318505dbeecf821ed7b9a9f86f422ed085e2d79c4fba0ae669920a"}, + {file = "pycryptodomex-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7cd39f7a110c1ab97ce9ee3459b8bc615920344dc00e56d1b709628965fba3f2"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e4eaaf6163ff13788c1f8f615ad60cdc69efac6d3bf7b310b21e8cfe5f46c801"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eac39e237d65981554c2d4c6668192dc7051ad61ab5fc383ed0ba049e4007ca2"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ab0d89d1761959b608952c7b347b0e76a32d1a5bb278afbaa10a7f3eaef9a0a"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e64164f816f5e43fd69f8ed98eb28f98157faf68208cd19c44ed9d8e72d33e8"}, + {file = "pycryptodomex-3.22.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f005de31efad6f9acefc417296c641f13b720be7dbfec90edeaca601c0fab048"}, + {file = "pycryptodomex-3.22.0.tar.gz", hash = "sha256:a1da61bacc22f93a91cbe690e3eb2022a03ab4123690ab16c46abb693a9df63d"}, ] [[package]] name = "pydantic" -version = "2.10.6" +version = "2.11.1" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "web"] files = [ - {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, - {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, + {file = "pydantic-2.11.1-py3-none-any.whl", hash = "sha256:5b6c415eee9f8123a14d859be0c84363fec6b1feb6b688d6435801230b56e0b8"}, + {file = "pydantic-2.11.1.tar.gz", hash = "sha256:442557d2910e75c991c39f4b4ab18963d57b9b55122c8b2a9cd176d8c29ce968"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" +pydantic-core = "2.33.0" typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -2661,112 +2665,111 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.0" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "web"] files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, + {file = "pydantic_core-2.33.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71dffba8fe9ddff628c68f3abd845e91b028361d43c5f8e7b3f8b91d7d85413e"}, + {file = "pydantic_core-2.33.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:abaeec1be6ed535a5d7ffc2e6c390083c425832b20efd621562fbb5bff6dc518"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:759871f00e26ad3709efc773ac37b4d571de065f9dfb1778012908bcc36b3a73"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dcfebee69cd5e1c0b76a17e17e347c84b00acebb8dd8edb22d4a03e88e82a207"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b1262b912435a501fa04cd213720609e2cefa723a07c92017d18693e69bf00b"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4726f1f3f42d6a25678c67da3f0b10f148f5655813c5aca54b0d1742ba821b8f"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e790954b5093dff1e3a9a2523fddc4e79722d6f07993b4cd5547825c3cbf97b5"}, + {file = "pydantic_core-2.33.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34e7fb3abe375b5c4e64fab75733d605dda0f59827752debc99c17cb2d5f3276"}, + {file = "pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ecb158fb9b9091b515213bed3061eb7deb1d3b4e02327c27a0ea714ff46b0760"}, + {file = "pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:4d9149e7528af8bbd76cc055967e6e04617dcb2a2afdaa3dea899406c5521faa"}, + {file = "pydantic_core-2.33.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e81a295adccf73477220e15ff79235ca9dcbcee4be459eb9d4ce9a2763b8386c"}, + {file = "pydantic_core-2.33.0-cp310-cp310-win32.whl", hash = "sha256:f22dab23cdbce2005f26a8f0c71698457861f97fc6318c75814a50c75e87d025"}, + {file = "pydantic_core-2.33.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cb2390355ba084c1ad49485d18449b4242da344dea3e0fe10babd1f0db7dcfc"}, + {file = "pydantic_core-2.33.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a608a75846804271cf9c83e40bbb4dab2ac614d33c6fd5b0c6187f53f5c593ef"}, + {file = "pydantic_core-2.33.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e1c69aa459f5609dec2fa0652d495353accf3eda5bdb18782bc5a2ae45c9273a"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9ec80eb5a5f45a2211793f1c4aeddff0c3761d1c70d684965c1807e923a588b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e925819a98318d17251776bd3d6aa9f3ff77b965762155bdad15d1a9265c4cfd"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bf68bb859799e9cec3d9dd8323c40c00a254aabb56fe08f907e437005932f2b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b2ea72dea0825949a045fa4071f6d5b3d7620d2a208335207793cf29c5a182d"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1583539533160186ac546b49f5cde9ffc928062c96920f58bd95de32ffd7bffd"}, + {file = "pydantic_core-2.33.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23c3e77bf8a7317612e5c26a3b084c7edeb9552d645742a54a5867635b4f2453"}, + {file = "pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7a7f2a3f628d2f7ef11cb6188bcf0b9e1558151d511b974dfea10a49afe192b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:f1fb026c575e16f673c61c7b86144517705865173f3d0907040ac30c4f9f5915"}, + {file = "pydantic_core-2.33.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:635702b2fed997e0ac256b2cfbdb4dd0bf7c56b5d8fba8ef03489c03b3eb40e2"}, + {file = "pydantic_core-2.33.0-cp311-cp311-win32.whl", hash = "sha256:07b4ced28fccae3f00626eaa0c4001aa9ec140a29501770a88dbbb0966019a86"}, + {file = "pydantic_core-2.33.0-cp311-cp311-win_amd64.whl", hash = "sha256:4927564be53239a87770a5f86bdc272b8d1fbb87ab7783ad70255b4ab01aa25b"}, + {file = "pydantic_core-2.33.0-cp311-cp311-win_arm64.whl", hash = "sha256:69297418ad644d521ea3e1aa2e14a2a422726167e9ad22b89e8f1130d68e1e9a"}, + {file = "pydantic_core-2.33.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6c32a40712e3662bebe524abe8abb757f2fa2000028d64cc5a1006016c06af43"}, + {file = "pydantic_core-2.33.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ec86b5baa36f0a0bfb37db86c7d52652f8e8aa076ab745ef7725784183c3fdd"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4deac83a8cc1d09e40683be0bc6d1fa4cde8df0a9bf0cda5693f9b0569ac01b6"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:175ab598fb457a9aee63206a1993874badf3ed9a456e0654273e56f00747bbd6"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f36afd0d56a6c42cf4e8465b6441cf546ed69d3a4ec92724cc9c8c61bd6ecf4"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a98257451164666afafc7cbf5fb00d613e33f7e7ebb322fbcd99345695a9a61"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecc6d02d69b54a2eb83ebcc6f29df04957f734bcf309d346b4f83354d8376862"}, + {file = "pydantic_core-2.33.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a69b7596c6603afd049ce7f3835bcf57dd3892fc7279f0ddf987bebed8caa5a"}, + {file = "pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ea30239c148b6ef41364c6f51d103c2988965b643d62e10b233b5efdca8c0099"}, + {file = "pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:abfa44cf2f7f7d7a199be6c6ec141c9024063205545aa09304349781b9a125e6"}, + {file = "pydantic_core-2.33.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20d4275f3c4659d92048c70797e5fdc396c6e4446caf517ba5cad2db60cd39d3"}, + {file = "pydantic_core-2.33.0-cp312-cp312-win32.whl", hash = "sha256:918f2013d7eadea1d88d1a35fd4a1e16aaf90343eb446f91cb091ce7f9b431a2"}, + {file = "pydantic_core-2.33.0-cp312-cp312-win_amd64.whl", hash = "sha256:aec79acc183865bad120b0190afac467c20b15289050648b876b07777e67ea48"}, + {file = "pydantic_core-2.33.0-cp312-cp312-win_arm64.whl", hash = "sha256:5461934e895968655225dfa8b3be79e7e927e95d4bd6c2d40edd2fa7052e71b6"}, + {file = "pydantic_core-2.33.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f00e8b59e1fc8f09d05594aa7d2b726f1b277ca6155fc84c0396db1b373c4555"}, + {file = "pydantic_core-2.33.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a73be93ecef45786d7d95b0c5e9b294faf35629d03d5b145b09b81258c7cd6d"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff48a55be9da6930254565ff5238d71d5e9cd8c5487a191cb85df3bdb8c77365"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a4ea04195638dcd8c53dadb545d70badba51735b1594810e9768c2c0b4a5da"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41d698dcbe12b60661f0632b543dbb119e6ba088103b364ff65e951610cb7ce0"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae62032ef513fe6281ef0009e30838a01057b832dc265da32c10469622613885"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f225f3a3995dbbc26affc191d0443c6c4aa71b83358fd4c2b7d63e2f6f0336f9"}, + {file = "pydantic_core-2.33.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bdd36b362f419c78d09630cbaebc64913f66f62bda6d42d5fbb08da8cc4f181"}, + {file = "pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2a0147c0bef783fd9abc9f016d66edb6cac466dc54a17ec5f5ada08ff65caf5d"}, + {file = "pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:c860773a0f205926172c6644c394e02c25421dc9a456deff16f64c0e299487d3"}, + {file = "pydantic_core-2.33.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:138d31e3f90087f42aa6286fb640f3c7a8eb7bdae829418265e7e7474bd2574b"}, + {file = "pydantic_core-2.33.0-cp313-cp313-win32.whl", hash = "sha256:d20cbb9d3e95114325780f3cfe990f3ecae24de7a2d75f978783878cce2ad585"}, + {file = "pydantic_core-2.33.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca1103d70306489e3d006b0f79db8ca5dd3c977f6f13b2c59ff745249431a606"}, + {file = "pydantic_core-2.33.0-cp313-cp313-win_arm64.whl", hash = "sha256:6291797cad239285275558e0a27872da735b05c75d5237bbade8736f80e4c225"}, + {file = "pydantic_core-2.33.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7b79af799630af263eca9ec87db519426d8c9b3be35016eddad1832bac812d87"}, + {file = "pydantic_core-2.33.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eabf946a4739b5237f4f56d77fa6668263bc466d06a8036c055587c130a46f7b"}, + {file = "pydantic_core-2.33.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8a1d581e8cdbb857b0e0e81df98603376c1a5c34dc5e54039dcc00f043df81e7"}, + {file = "pydantic_core-2.33.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:7c9c84749f5787781c1c45bb99f433402e484e515b40675a5d121ea14711cf61"}, + {file = "pydantic_core-2.33.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:64672fa888595a959cfeff957a654e947e65bbe1d7d82f550417cbd6898a1d6b"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26bc7367c0961dec292244ef2549afa396e72e28cc24706210bd44d947582c59"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce72d46eb201ca43994303025bd54d8a35a3fc2a3495fac653d6eb7205ce04f4"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14229c1504287533dbf6b1fc56f752ce2b4e9694022ae7509631ce346158de11"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:085d8985b1c1e48ef271e98a658f562f29d89bda98bf120502283efbc87313eb"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31860fbda80d8f6828e84b4a4d129fd9c4535996b8249cfb8c720dc2a1a00bb8"}, + {file = "pydantic_core-2.33.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f200b2f20856b5a6c3a35f0d4e344019f805e363416e609e9b47c552d35fd5ea"}, + {file = "pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f72914cfd1d0176e58ddc05c7a47674ef4222c8253bf70322923e73e14a4ac3"}, + {file = "pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:91301a0980a1d4530d4ba7e6a739ca1a6b31341252cb709948e0aca0860ce0ae"}, + {file = "pydantic_core-2.33.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7419241e17c7fbe5074ba79143d5523270e04f86f1b3a0dff8df490f84c8273a"}, + {file = "pydantic_core-2.33.0-cp39-cp39-win32.whl", hash = "sha256:7a25493320203005d2a4dac76d1b7d953cb49bce6d459d9ae38e30dd9f29bc9c"}, + {file = "pydantic_core-2.33.0-cp39-cp39-win_amd64.whl", hash = "sha256:82a4eba92b7ca8af1b7d5ef5f3d9647eee94d1f74d21ca7c21e3a2b92e008358"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2762c568596332fdab56b07060c8ab8362c56cf2a339ee54e491cd503612c50"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bf637300ff35d4f59c006fff201c510b2b5e745b07125458a5389af3c0dff8c"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c151ce3d59ed56ebd7ce9ce5986a409a85db697d25fc232f8e81f195aa39a1"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee65f0cc652261744fd07f2c6e6901c914aa6c5ff4dcfaf1136bc394d0dd26b"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:024d136ae44d233e6322027bbf356712b3940bee816e6c948ce4b90f18471b3d"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e37f10f6d4bc67c58fbd727108ae1d8b92b397355e68519f1e4a7babb1473442"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:502ed542e0d958bd12e7c3e9a015bce57deaf50eaa8c2e1c439b512cb9db1e3a"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:715c62af74c236bf386825c0fdfa08d092ab0f191eb5b4580d11c3189af9d330"}, + {file = "pydantic_core-2.33.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bccc06fa0372151f37f6b69834181aa9eb57cf8665ed36405fb45fbf6cac3bae"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5d8dc9f63a26f7259b57f46a7aab5af86b2ad6fbe48487500bb1f4b27e051e4c"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30369e54d6d0113d2aa5aee7a90d17f225c13d87902ace8fcd7bbf99b19124db"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb479354c62067afa62f53bb387827bee2f75c9c79ef25eef6ab84d4b1ae3b"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0310524c833d91403c960b8a3cf9f46c282eadd6afd276c8c5edc617bd705dc9"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eddb18a00bbb855325db27b4c2a89a4ba491cd6a0bd6d852b225172a1f54b36c"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ade5dbcf8d9ef8f4b28e682d0b29f3008df9842bb5ac48ac2c17bc55771cc976"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2c0afd34f928383e3fd25740f2050dbac9d077e7ba5adbaa2227f4d4f3c8da5c"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7da333f21cd9df51d5731513a6d39319892947604924ddf2e24a4612975fb936"}, + {file = "pydantic_core-2.33.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b6d77c75a57f041c5ee915ff0b0bb58eabb78728b69ed967bc5b780e8f701b8"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba95691cf25f63df53c1d342413b41bd7762d9acb425df8858d7efa616c0870e"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f1ab031feb8676f6bd7c85abec86e2935850bf19b84432c64e3e239bffeb1ec"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58c1151827eef98b83d49b6ca6065575876a02d2211f259fb1a6b7757bd24dd8"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66d931ea2c1464b738ace44b7334ab32a2fd50be023d863935eb00f42be1778"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0bcf0bab28995d483f6c8d7db25e0d05c3efa5cebfd7f56474359e7137f39856"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:89670d7a0045acb52be0566df5bc8b114ac967c662c06cf5e0c606e4aadc964b"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:b716294e721d8060908dbebe32639b01bfe61b15f9f57bcc18ca9a0e00d9520b"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fc53e05c16697ff0c1c7c2b98e45e131d4bfb78068fffff92a82d169cbb4c7b7"}, + {file = "pydantic_core-2.33.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:68504959253303d3ae9406b634997a2123a0b0c1da86459abbd0ffc921695eac"}, + {file = "pydantic_core-2.33.0.tar.gz", hash = "sha256:40eb8af662ba409c3cbf4a8150ad32ae73514cd7cb1f1a2113af39763dd616b3"}, ] [package.dependencies] @@ -2774,14 +2777,14 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.8.0" +version = "2.8.1" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" groups = ["main", "web"] files = [ - {file = "pydantic_settings-2.8.0-py3-none-any.whl", hash = "sha256:c782c7dc3fb40e97b238e713c25d26f64314aece2e91abcff592fcac15f71820"}, - {file = "pydantic_settings-2.8.0.tar.gz", hash = "sha256:88e2ca28f6e68ea102c99c3c401d6c9078e68a5df600e97b43891c34e089500a"}, + {file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"}, + {file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"}, ] [package.dependencies] @@ -2831,33 +2834,34 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pyopenssl" -version = "24.2.1" +version = "25.0.0" description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"}, - {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"}, + {file = "pyOpenSSL-25.0.0-py3-none-any.whl", hash = "sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90"}, + {file = "pyopenssl-25.0.0.tar.gz", hash = "sha256:cd2cef799efa3936bb08e8ccb9433a575722b9dd986023f1cabc4ae64e9dac16"}, ] [package.dependencies] -cryptography = ">=41.0.5,<44" +cryptography = ">=41.0.5,<45" +typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""} [package.extras] -docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx-rtd-theme"] +docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] [[package]] name = "pyparsing" -version = "3.2.1" +version = "3.2.3" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" groups = ["main", "web"] files = [ - {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, - {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, + {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, + {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, ] [package.extras] @@ -2890,14 +2894,14 @@ files = [ [[package]] name = "pytest" -version = "8.3.4" +version = "8.3.5" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, ] [package.dependencies] @@ -2930,6 +2934,18 @@ pytest = ">=8.2,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "python-bitcoinlib" +version = "0.12.2" +description = "The Swiss Army Knife of the Bitcoin protocol." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "python-bitcoinlib-0.12.2.tar.gz", hash = "sha256:c65ab61427c77c38d397bfc431f71d86fd355b453a536496ec3fcb41bd10087d"}, + {file = "python_bitcoinlib-0.12.2-py3-none-any.whl", hash = "sha256:2f29a9f475f21c12169b3a6cc8820f34f11362d7ff1200a5703dce3e4e903a44"}, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2947,14 +2963,14 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.0" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main", "web"] files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, ] [package.extras] @@ -2997,14 +3013,14 @@ requests = ">=2.28" [[package]] name = "pytz" -version = "2025.1" +version = "2025.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, - {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] [[package]] @@ -3246,16 +3262,47 @@ files = [ [package.dependencies] six = ">=1.7.0" +[[package]] +name = "rfc3161-client" +version = "1.0.1" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "rfc3161_client-1.0.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:75d8c9d255fa79b9ae4aa27cee519893599efd79f9e6c24a1194dd296ce1c210"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0d3db059fe08d8b6b06aff89e133fcc352ffea1a1dafadb116dda9dae59d0689"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdef0c9d3213ca5b79d7f76ada48ae10c5011cb25abed2f6df07b344d16d1c28"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7c34ce4d7d2bf5207c54de3a771e757f1f8bb04a8469d3cef6aefe074841064d"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4809f2fcfb5f8b42261a7b831929f62a297b584c8d1f4d242eae5e9447674b6"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a644b220b7f0f0be7856f49b043651982bd76e7aa9eb17b3e4e303fde36ed5a1"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:bb03a5a77b07adf766b7daac6cb8b7a8337ffc8f6d6046af74469973f52df8e1"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:d6c6e4626780b1c531d32d6a126d6c27865b1eb59c65e8b0f1f8f94aa3205285"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:912c2f049ce23d0f1c173b6fbd8673f964a27ad97907064dbc74f86dd0d95d15"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:081211a1b602b6dff7feb314d39ca2229c8db4e8cf55eef0c35b460470f4b2bb"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-win32.whl", hash = "sha256:59efa8fddf72a15e397276fe512dbfb99c0dc95032b495815bfc4f8f16302f2c"}, + {file = "rfc3161_client-1.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:5381a63d5ed5b3c257cb18aacf3f737b1a1ad6df634290fe689b6d601c61cd24"}, + {file = "rfc3161_client-1.0.1.tar.gz", hash = "sha256:1c951f3912b90c6d3f3505e644b74ee08543387253647b86459addbffb16f63f"}, +] + +[package.dependencies] +cryptography = ">=43,<45" + +[package.extras] +dev = ["maturin (>=1.7,<2.0)", "rfc3161-client[doc,lint,test]"] +lint = ["interrogate", "ruff (>=0.7,<0.12)"] +test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] + [[package]] name = "rich" -version = "13.9.4" +version = "14.0.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["main"] files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, + {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, + {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, ] [package.dependencies] @@ -3374,32 +3421,32 @@ files = [ [[package]] name = "s3transfer" -version = "0.11.2" +version = "0.11.4" description = "An Amazon S3 Transfer Manager" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "s3transfer-0.11.2-py3-none-any.whl", hash = "sha256:be6ecb39fadd986ef1701097771f87e4d2f821f27f6071c872143884d2950fbc"}, - {file = "s3transfer-0.11.2.tar.gz", hash = "sha256:3b39185cb72f5acc77db1a58b6e25b977f28d20496b6e58d6813d75f464d632f"}, + {file = "s3transfer-0.11.4-py3-none-any.whl", hash = "sha256:ac265fa68318763a03bf2dc4f39d5cbd6a9e178d81cc9483ad27da33637e320d"}, + {file = "s3transfer-0.11.4.tar.gz", hash = "sha256:559f161658e1cf0a911f45940552c696735f5c74e64362e515f333ebed87d679"}, ] [package.dependencies] -botocore = ">=1.36.0,<2.0a.0" +botocore = ">=1.37.4,<2.0a.0" [package.extras] -crt = ["botocore[crt] (>=1.36.0,<2.0a.0)"] +crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"] [[package]] name = "selenium" -version = "4.29.0" +version = "4.30.0" description = "Official Python bindings for Selenium WebDriver" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "selenium-4.29.0-py3-none-any.whl", hash = "sha256:ce5d26f1ddc1111641113653af33694c13947dd36c2df09cdd33f554351d372e"}, - {file = "selenium-4.29.0.tar.gz", hash = "sha256:3a62f7ec33e669364a6c0562a701deb69745b569c50d55f1a912bf8eb33358ba"}, + {file = "selenium-4.30.0-py3-none-any.whl", hash = "sha256:90bcd3be86a1762100a093b33e5e4530b328226da94208caadb15ce13243dffd"}, + {file = "selenium-4.30.0.tar.gz", hash = "sha256:16ab890fc7cb21a01e1b1e9a0fbaa9445fe30837eabc66e90b3bacf12138126a"}, ] [package.dependencies] @@ -3412,14 +3459,14 @@ websocket-client = ">=1.8,<2.0" [[package]] name = "setuptools" -version = "75.8.0" +version = "75.9.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" groups = ["worker"] files = [ - {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, - {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, + {file = "setuptools-75.9.1-py3-none-any.whl", hash = "sha256:0a6f876d62f4d978ca1a11ab4daf728d1357731f978543ff18ecdbf9fd071f73"}, + {file = "setuptools-75.9.1.tar.gz", hash = "sha256:b6eca2c3070cdc82f71b4cb4bb2946bc0760a210d11362278cf1ff394e6ea32c"}, ] [package.extras] @@ -3481,81 +3528,81 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.38" +version = "2.0.40" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main", "web"] files = [ - {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-win32.whl", hash = "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297"}, - {file = "SQLAlchemy-2.0.38-cp310-cp310-win_amd64.whl", hash = "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-win32.whl", hash = "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba"}, - {file = "SQLAlchemy-2.0.38-cp311-cp311-win_amd64.whl", hash = "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725"}, - {file = "SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120"}, - {file = "SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-win32.whl", hash = "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079"}, - {file = "SQLAlchemy-2.0.38-cp37-cp37m-win_amd64.whl", hash = "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-win32.whl", hash = "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63"}, - {file = "SQLAlchemy-2.0.38-cp38-cp38-win_amd64.whl", hash = "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-win32.whl", hash = "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc"}, - {file = "SQLAlchemy-2.0.38-cp39-cp39-win_amd64.whl", hash = "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1"}, - {file = "SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753"}, - {file = "sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ae9597cab738e7cc823f04a704fb754a9249f0b6695a6aeb63b74055cd417a96"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a5c21ab099a83d669ebb251fddf8f5cee4d75ea40a5a1653d9c43d60e20867"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bece9527f5a98466d67fb5d34dc560c4da964240d8b09024bb21c1246545e04e"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:8bb131ffd2165fae48162c7bbd0d97c84ab961deea9b8bab16366543deeab625"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9408fd453d5f8990405cc9def9af46bfbe3183e6110401b407c2d073c3388f47"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-win32.whl", hash = "sha256:00a494ea6f42a44c326477b5bee4e0fc75f6a80c01570a32b57e89cf0fbef85a"}, + {file = "SQLAlchemy-2.0.40-cp37-cp37m-win_amd64.whl", hash = "sha256:c7b927155112ac858357ccf9d255dd8c044fd9ad2dc6ce4c4149527c901fa4c3"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1ea21bef99c703f44444ad29c2c1b6bd55d202750b6de8e06a955380f4725d7"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:afe63b208153f3a7a2d1a5b9df452b0673082588933e54e7c8aac457cf35e758"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8aae085ea549a1eddbc9298b113cffb75e514eadbb542133dd2b99b5fb3b6af"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ea9181284754d37db15156eb7be09c86e16e50fbe77610e9e7bee09291771a1"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5434223b795be5c5ef8244e5ac98056e290d3a99bdcc539b916e282b160dda00"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15d08d5ef1b779af6a0909b97be6c1fd4298057504eb6461be88bd1696cb438e"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-win32.whl", hash = "sha256:cd2f75598ae70bcfca9117d9e51a3b06fe29edd972fdd7fd57cc97b4dbf3b08a"}, + {file = "sqlalchemy-2.0.40-cp310-cp310-win_amd64.whl", hash = "sha256:2cbafc8d39ff1abdfdda96435f38fab141892dc759a2165947d1a8fffa7ef596"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6bacab7514de6146a1976bc56e1545bee247242fab030b89e5f70336fc0003e"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5654d1ac34e922b6c5711631f2da497d3a7bffd6f9f87ac23b35feea56098011"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35904d63412db21088739510216e9349e335f142ce4a04b69e2528020ee19ed4"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7a80ed86d6aaacb8160a1caef6680d4ddd03c944d985aecee940d168c411d1"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:519624685a51525ddaa7d8ba8265a1540442a2ec71476f0e75241eb8263d6f51"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5f9999a5b0e9689bed96e60ee53c3384f1a05c2dd8068cc2e8361b0df5b7a"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-win32.whl", hash = "sha256:c0cae71e20e3c02c52f6b9e9722bca70e4a90a466d59477822739dc31ac18b4b"}, + {file = "sqlalchemy-2.0.40-cp311-cp311-win_amd64.whl", hash = "sha256:574aea2c54d8f1dd1699449f332c7d9b71c339e04ae50163a3eb5ce4c4325ee4"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d3b31d0a1c44b74d3ae27a3de422dfccd2b8f0b75e51ecb2faa2bf65ab1ba0d"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f7a0f506cf78c80450ed1e816978643d3969f99c4ac6b01104a6fe95c5490a"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bb933a650323e476a2e4fbef8997a10d0003d4da996aad3fd7873e962fdde4d"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959738971b4745eea16f818a2cd086fb35081383b078272c35ece2b07012716"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:110179728e442dae85dd39591beb74072ae4ad55a44eda2acc6ec98ead80d5f2"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8040680eaacdce4d635f12c55c714f3d4c7f57da2bc47a01229d115bd319191"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-win32.whl", hash = "sha256:650490653b110905c10adac69408380688cefc1f536a137d0d69aca1069dc1d1"}, + {file = "sqlalchemy-2.0.40-cp312-cp312-win_amd64.whl", hash = "sha256:2be94d75ee06548d2fc591a3513422b873490efb124048f50556369a834853b0"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500"}, + {file = "sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50f5885bbed261fc97e2e66c5156244f9704083a674b8d17f24c72217d29baf5"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf0e99cdb600eabcd1d65cdba0d3c91418fee21c4aa1d28db47d095b1064a7d8"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe147fcd85aaed53ce90645c91ed5fca0cc88a797314c70dfd9d35925bd5d106"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf7cee56bd552385c1ee39af360772fbfc2f43be005c78d1140204ad6148438"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4aeb939bcac234b88e2d25d5381655e8353fe06b4e50b1c55ecffe56951d18c2"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c268b5100cfeaa222c40f55e169d484efa1384b44bf9ca415eae6d556f02cb08"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-win32.whl", hash = "sha256:46628ebcec4f23a1584fb52f2abe12ddb00f3bb3b7b337618b80fc1b51177aff"}, + {file = "sqlalchemy-2.0.40-cp38-cp38-win_amd64.whl", hash = "sha256:7e0505719939e52a7b0c65d20e84a6044eb3712bb6f239c6b1db77ba8e173a37"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c884de19528e0fcd9dc34ee94c810581dd6e74aef75437ff17e696c2bfefae3e"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1abb387710283fc5983d8a1209d9696a4eae9db8d7ac94b402981fe2fe2e39ad"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cfa124eda500ba4b0d3afc3e91ea27ed4754e727c7f025f293a22f512bcd4c9"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b6b28d303b9d57c17a5164eb1fd2d5119bb6ff4413d5894e74873280483eeb5"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b5a5bbe29c10c5bfd63893747a1bf6f8049df607638c786252cb9243b86b6706"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f0fda83e113bb0fb27dc003685f32a5dcb99c9c4f41f4fa0838ac35265c23b5c"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-win32.whl", hash = "sha256:957f8d85d5e834397ef78a6109550aeb0d27a53b5032f7a57f2451e1adc37e98"}, + {file = "sqlalchemy-2.0.40-cp39-cp39-win_amd64.whl", hash = "sha256:1ffdf9c91428e59744f8e6f98190516f8e1d05eec90e936eb08b257332c5e870"}, + {file = "sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a"}, + {file = "sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (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\")"} +greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (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\")"} typing-extensions = ">=4.6.0" [package.extras] -aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] -aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] -asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"] +aioodbc = ["aioodbc", "greenlet (>=1)"] +aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"] +asyncio = ["greenlet (>=1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] @@ -3566,7 +3613,7 @@ mysql-connector = ["mysql-connector-python"] oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-asyncpg = ["asyncpg", "greenlet (>=1)"] postgresql-pg8000 = ["pg8000 (>=1.29.1)"] postgresql-psycopg = ["psycopg (>=3.0.7)"] postgresql-psycopg2binary = ["psycopg2-binary"] @@ -3577,14 +3624,14 @@ sqlcipher = ["sqlcipher3_binary"] [[package]] name = "starlette" -version = "0.45.3" +version = "0.46.1" description = "The little ASGI library that shines." optional = false python-versions = ">=3.9" groups = ["web"] files = [ - {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, - {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, + {file = "starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227"}, + {file = "starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230"}, ] [package.dependencies] @@ -3712,14 +3759,14 @@ sortedcontainers = "*" [[package]] name = "trio-websocket" -version = "0.12.1" +version = "0.12.2" description = "WebSocket library for Trio" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "trio_websocket-0.12.1-py3-none-any.whl", hash = "sha256:608ec746bb287e5d5a66baf483e41194193c5cf05ffaad6240e7d1fcd80d1e6f"}, - {file = "trio_websocket-0.12.1.tar.gz", hash = "sha256:d55ccd4d3eae27c494f3fdae14823317839bdcb8214d1173eacc4d42c69fc91b"}, + {file = "trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"}, + {file = "trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae"}, ] [package.dependencies] @@ -3728,36 +3775,16 @@ outcome = ">=1.2.0" trio = ">=0.11" wsproto = ">=0.14" -[[package]] -name = "tsp-client" -version = "0.2.0" -description = "An IETF Time-Stamp Protocol (TSP) (RFC 3161) client" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "tsp-client-0.2.0.tar.gz", hash = "sha256:6e66148dd116322eb44a7484e5ad33bbe640b997343c443de9cc70fc5eb19987"}, - {file = "tsp_client-0.2.0-py3-none-any.whl", hash = "sha256:0b790d10a68d66782c13f1d7cc7f5206df26b49826c1da80944b7c05b1731784"}, -] - -[package.dependencies] -asn1crypto = ">=0.24.0" -pyOpenSSL = ">=20.0.0" -requests = ">=2.18.4" - -[package.extras] -tests = ["build", "coverage", "mypy", "ruff", "wheel"] - [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" groups = ["main", "dev", "web"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5"}, + {file = "typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b"}, ] [[package]] @@ -3776,28 +3803,44 @@ files = [ mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" +[[package]] +name = "typing-inspection" +version = "0.4.0" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main", "web"] +files = [ + {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, + {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "tzdata" -version = "2025.1" +version = "2025.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] +markers = "platform_system == \"Windows\"" files = [ - {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, - {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, ] [[package]] name = "tzlocal" -version = "5.3" +version = "5.3.1" description = "tzinfo object for the local timezone" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c"}, - {file = "tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2"}, + {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, + {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, ] [package.dependencies] @@ -3873,14 +3916,14 @@ files = [ [[package]] name = "virtualenv" -version = "20.29.2" +version = "20.30.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, - {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, + {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, + {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, ] [package.dependencies] @@ -3890,7 +3933,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [[package]] name = "vk-api" @@ -4032,81 +4075,81 @@ test = ["websockets"] [[package]] name = "websockets" -version = "15.0" +version = "15.0.1" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "websockets-15.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5e6ee18a53dd5743e6155b8ff7e8e477c25b29b440f87f65be8165275c87fef0"}, - {file = "websockets-15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ee06405ea2e67366a661ed313e14cf2a86e84142a3462852eb96348f7219cee3"}, - {file = "websockets-15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8711682a629bbcaf492f5e0af72d378e976ea1d127a2d47584fa1c2c080b436b"}, - {file = "websockets-15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94c4a9b01eede952442c088d415861b0cf2053cbd696b863f6d5022d4e4e2453"}, - {file = "websockets-15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45535fead66e873f411c1d3cf0d3e175e66f4dd83c4f59d707d5b3e4c56541c4"}, - {file = "websockets-15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e389efe46ccb25a1f93d08c7a74e8123a2517f7b7458f043bd7529d1a63ffeb"}, - {file = "websockets-15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:67a04754d121ea5ca39ddedc3f77071651fb5b0bc6b973c71c515415b44ed9c5"}, - {file = "websockets-15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:bd66b4865c8b853b8cca7379afb692fc7f52cf898786537dfb5e5e2d64f0a47f"}, - {file = "websockets-15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a4cc73a6ae0a6751b76e69cece9d0311f054da9b22df6a12f2c53111735657c8"}, - {file = "websockets-15.0-cp310-cp310-win32.whl", hash = "sha256:89da58e4005e153b03fe8b8794330e3f6a9774ee9e1c3bd5bc52eb098c3b0c4f"}, - {file = "websockets-15.0-cp310-cp310-win_amd64.whl", hash = "sha256:4ff380aabd7a74a42a760ee76c68826a8f417ceb6ea415bd574a035a111fd133"}, - {file = "websockets-15.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dd24c4d256558429aeeb8d6c24ebad4e982ac52c50bc3670ae8646c181263965"}, - {file = "websockets-15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f83eca8cbfd168e424dfa3b3b5c955d6c281e8fc09feb9d870886ff8d03683c7"}, - {file = "websockets-15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4095a1f2093002c2208becf6f9a178b336b7572512ee0a1179731acb7788e8ad"}, - {file = "websockets-15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb915101dfbf318486364ce85662bb7b020840f68138014972c08331458d41f3"}, - {file = "websockets-15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:45d464622314973d78f364689d5dbb9144e559f93dca11b11af3f2480b5034e1"}, - {file = "websockets-15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ace960769d60037ca9625b4c578a6f28a14301bd2a1ff13bb00e824ac9f73e55"}, - {file = "websockets-15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7cd4b1015d2f60dfe539ee6c95bc968d5d5fad92ab01bb5501a77393da4f596"}, - {file = "websockets-15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f7290295794b5dec470867c7baa4a14182b9732603fd0caf2a5bf1dc3ccabf3"}, - {file = "websockets-15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3abd670ca7ce230d5a624fd3d55e055215d8d9b723adee0a348352f5d8d12ff4"}, - {file = "websockets-15.0-cp311-cp311-win32.whl", hash = "sha256:110a847085246ab8d4d119632145224d6b49e406c64f1bbeed45c6f05097b680"}, - {file = "websockets-15.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7bbbe2cd6ed80aceef2a14e9f1c1b61683194c216472ed5ff33b700e784e37"}, - {file = "websockets-15.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cccc18077acd34c8072578394ec79563664b1c205f7a86a62e94fafc7b59001f"}, - {file = "websockets-15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4c22992e24f12de340ca5f824121a5b3e1a37ad4360b4e1aaf15e9d1c42582d"}, - {file = "websockets-15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1206432cc6c644f6fc03374b264c5ff805d980311563202ed7fef91a38906276"}, - {file = "websockets-15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3cc75ef3e17490042c47e0523aee1bcc4eacd2482796107fd59dd1100a44bc"}, - {file = "websockets-15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b89504227a5311610e4be16071465885a0a3d6b0e82e305ef46d9b064ce5fb72"}, - {file = "websockets-15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56e3efe356416bc67a8e093607315951d76910f03d2b3ad49c4ade9207bf710d"}, - {file = "websockets-15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f2205cdb444a42a7919690238fb5979a05439b9dbb73dd47c863d39640d85ab"}, - {file = "websockets-15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aea01f40995fa0945c020228ab919b8dfc93fc8a9f2d3d705ab5b793f32d9e99"}, - {file = "websockets-15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9f8e33747b1332db11cf7fcf4a9512bef9748cb5eb4d3f7fbc8c30d75dc6ffc"}, - {file = "websockets-15.0-cp312-cp312-win32.whl", hash = "sha256:32e02a2d83f4954aa8c17e03fe8ec6962432c39aca4be7e8ee346b05a3476904"}, - {file = "websockets-15.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc02b159b65c05f2ed9ec176b715b66918a674bd4daed48a9a7a590dd4be1aa"}, - {file = "websockets-15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d2244d8ab24374bed366f9ff206e2619345f9cd7fe79aad5225f53faac28b6b1"}, - {file = "websockets-15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a302241fbe825a3e4fe07666a2ab513edfdc6d43ce24b79691b45115273b5e7"}, - {file = "websockets-15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:10552fed076757a70ba2c18edcbc601c7637b30cdfe8c24b65171e824c7d6081"}, - {file = "websockets-15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c53f97032b87a406044a1c33d1e9290cc38b117a8062e8a8b285175d7e2f99c9"}, - {file = "websockets-15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1caf951110ca757b8ad9c4974f5cac7b8413004d2f29707e4d03a65d54cedf2b"}, - {file = "websockets-15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf1ab71f9f23b0a1d52ec1682a3907e0c208c12fef9c3e99d2b80166b17905f"}, - {file = "websockets-15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bfcd3acc1a81f106abac6afd42327d2cf1e77ec905ae11dc1d9142a006a496b6"}, - {file = "websockets-15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8c5c8e1bac05ef3c23722e591ef4f688f528235e2480f157a9cfe0a19081375"}, - {file = "websockets-15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:86bfb52a9cfbcc09aba2b71388b0a20ea5c52b6517c0b2e316222435a8cdab72"}, - {file = "websockets-15.0-cp313-cp313-win32.whl", hash = "sha256:26ba70fed190708551c19a360f9d7eca8e8c0f615d19a574292b7229e0ae324c"}, - {file = "websockets-15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8"}, - {file = "websockets-15.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c348abc5924caa02a62896300e32ea80a81521f91d6db2e853e6b1994017c9f6"}, - {file = "websockets-15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5294fcb410ed0a45d5d1cdedc4e51a60aab5b2b3193999028ea94afc2f554b05"}, - {file = "websockets-15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c24ba103ecf45861e2e1f933d40b2d93f5d52d8228870c3e7bf1299cd1cb8ff1"}, - {file = "websockets-15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc8821a03bcfb36e4e4705316f6b66af28450357af8a575dc8f4b09bf02a3dee"}, - {file = "websockets-15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc5ae23ada6515f31604f700009e2df90b091b67d463a8401c1d8a37f76c1d7"}, - {file = "websockets-15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ac67b542505186b3bbdaffbc303292e1ee9c8729e5d5df243c1f20f4bb9057e"}, - {file = "websockets-15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c86dc2068f1c5ca2065aca34f257bbf4f78caf566eb230f692ad347da191f0a1"}, - {file = "websockets-15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:30cff3ef329682b6182c01c568f551481774c476722020b8f7d0daacbed07a17"}, - {file = "websockets-15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:98dcf978d4c6048965d1762abd534c9d53bae981a035bfe486690ba11f49bbbb"}, - {file = "websockets-15.0-cp39-cp39-win32.whl", hash = "sha256:37d66646f929ae7c22c79bc73ec4074d6db45e6384500ee3e0d476daf55482a9"}, - {file = "websockets-15.0-cp39-cp39-win_amd64.whl", hash = "sha256:24d5333a9b2343330f0f4eb88546e2c32a7f5c280f8dd7d3cc079beb0901781b"}, - {file = "websockets-15.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b499caef4bca9cbd0bd23cd3386f5113ee7378094a3cb613a2fa543260fe9506"}, - {file = "websockets-15.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:17f2854c6bd9ee008c4b270f7010fe2da6c16eac5724a175e75010aacd905b31"}, - {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89f72524033abbfde880ad338fd3c2c16e31ae232323ebdfbc745cbb1b3dcc03"}, - {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1657a9eecb29d7838e3b415458cc494e6d1b194f7ac73a34aa55c6fb6c72d1f3"}, - {file = "websockets-15.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e413352a921f5ad5d66f9e2869b977e88d5103fc528b6deb8423028a2befd842"}, - {file = "websockets-15.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8561c48b0090993e3b2a54db480cab1d23eb2c5735067213bb90f402806339f5"}, - {file = "websockets-15.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:190bc6ef8690cd88232a038d1b15714c258f79653abad62f7048249b09438af3"}, - {file = "websockets-15.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:327adab7671f3726b0ba69be9e865bba23b37a605b585e65895c428f6e47e766"}, - {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd8ef197c87afe0a9009f7a28b5dc613bfc585d329f80b7af404e766aa9e8c7"}, - {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:789c43bf4a10cd067c24c321238e800b8b2716c863ddb2294d2fed886fa5a689"}, - {file = "websockets-15.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7394c0b7d460569c9285fa089a429f58465db930012566c03046f9e3ab0ed181"}, - {file = "websockets-15.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ea4f210422b912ebe58ef0ad33088bc8e5c5ff9655a8822500690abc3b1232d"}, - {file = "websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3"}, - {file = "websockets-15.0.tar.gz", hash = "sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, ] [[package]] @@ -4142,27 +4185,39 @@ h11 = ">=0.9.0,<1" [[package]] name = "yt-dlp" -version = "2025.2.19" +version = "2025.3.31" description = "A feature-rich command-line audio/video downloader" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "yt_dlp-2025.2.19-py3-none-any.whl", hash = "sha256:3ed218eaeece55e9d715afd41abc450dc406ee63bf79355169dfde312d38fdb8"}, - {file = "yt_dlp-2025.2.19.tar.gz", hash = "sha256:f33ca76df2e4db31880f2fe408d44f5058d9f135015b13e50610dfbe78245bea"}, + {file = "yt_dlp-2025.3.31-py3-none-any.whl", hash = "sha256:8ecb3aa218a3bebe431119f513a8972b9b9d992edf67168c00ab92329a03baec"}, + {file = "yt_dlp-2025.3.31.tar.gz", hash = "sha256:1bfe0e660d1a70a09e27b2d58f92e30b1e2e362cc487829f2f824346ae49fb91"}, ] [package.extras] build = ["build", "hatchling", "pip", "setuptools (>=71.0.2)", "wheel"] -curl-cffi = ["curl-cffi (==0.5.10) ; os_name == \"nt\" and implementation_name == \"cpython\"", "curl-cffi (>=0.5.10,!=0.6.*,<0.7.2) ; os_name != \"nt\" and implementation_name == \"cpython\""] +curl-cffi = ["curl-cffi (>=0.5.10,<0.6.dev0 || ==0.10.*) ; implementation_name == \"cpython\""] default = ["brotli ; implementation_name == \"cpython\"", "brotlicffi ; implementation_name != \"cpython\"", "certifi", "mutagen", "pycryptodomex", "requests (>=2.32.2,<3)", "urllib3 (>=1.26.17,<3)", "websockets (>=13.0)"] -dev = ["autopep8 (>=2.0,<3.0)", "pre-commit", "pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)", "ruff (>=0.9.0,<0.10.0)"] +dev = ["autopep8 (>=2.0,<3.0)", "pre-commit", "pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)", "ruff (>=0.11.0,<0.12.0)"] pyinstaller = ["pyinstaller (>=6.11.1)"] secretstorage = ["cffi", "secretstorage"] -static-analysis = ["autopep8 (>=2.0,<3.0)", "ruff (>=0.9.0,<0.10.0)"] +static-analysis = ["autopep8 (>=2.0,<3.0)", "ruff (>=0.11.0,<0.12.0)"] test = ["pytest (>=8.1,<9.0)", "pytest-rerunfailures (>=14.0,<15.0)"] +[[package]] +name = "yt-dlp-get-pot" +version = "0.3.0" +description = "" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "yt_dlp_get_pot-0.3.0-py3-none-any.whl", hash = "sha256:a49a596a3e3c02cd9ce051192ea3fe8168cf24ece8954bed6aa331a87d86954f"}, + {file = "yt_dlp_get_pot-0.3.0.tar.gz", hash = "sha256:ac9530b9e7b3d667235b9119da475f595d2dc7e6f6bbf98b965011be454e8833"}, +] + [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.13" -content-hash = "7efbba5ed47fe36f6767dd60e4578c1db1ac830193b39d230a1e5e6fb4ccb01c" +content-hash = "4e1754247809ac79cab412fab3900ffa88b6bc02d48fe4971d960896e0b72dda" diff --git a/pyproject.toml b/pyproject.toml index 4ba95f9..7b3044d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,6 @@ requires-python = ">=3.10,<3.13" dependencies = [ "auto-archiver (>=0.13.1)", - "oscrypto @ git+https://github.com/wbond/oscrypto.git@d5f3437ed24257895ae1edd9e503cfb352e635a8", "celery (>=5.0)", "redis (==3.5.3)", "loguru (>=0.7.3,<0.8.0)", From c65f3fe121c5f14d74fc2005c8978e8ef6254c08 Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:09:35 +0100 Subject: [PATCH 34/40] renamed configs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16ed669..7ebe7a1 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ To properly set up the API you need to install `docker` and to have these files, 2. a `user-groups.yaml` to manage user permissions 1. note that all local files referenced in `user-groups.yaml` and any orchestration.yaml files should be relative to the home directory so if your service account is in `secrets/orchestration.yaml` use that path and not just `orchestration.yaml`. 2. go through the example file and configure it according to your needs. -3. you will need to create and reference at least one `secrets/orchestration.yaml` file, you can do so by following the instructions in the [auto-archiver](https://github.com/bellingcat/auto-archiver#installation) that automatically generates one for you. If you use the archive sheets feature you will need to create a `orchestrationsheets-sheets.yaml` file as well that should have the `gsheet_feeder` and `gsheet_db` enabled and configured, the auto-archiver has [extensive documentation](https://auto-archiver.readthedocs.io/en/latest/) on how to set this up. +3. you will need to create and reference at least one `secrets/orchestration.yaml` file, you can do so by following the instructions in the [auto-archiver](https://github.com/bellingcat/auto-archiver#installation) that automatically generates one for you. If you use the archive sheets feature you will need to create a `orchestrationsheets-sheets.yaml` file as well that should have the `gsheet_feeder_db` feeder and database enabled and configured, the auto-archiver has [extensive documentation](https://auto-archiver.readthedocs.io/en/latest/) on how to set this up. Do not commit those files, they are .gitignored by default. We also advise you to keep any sensitive files in the `secrets/` folder which is pinned and gitignored. From fd799c8fbcb660d300d9e47a40c0cfbcb424bfbe Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:13:58 +0100 Subject: [PATCH 35/40] fix orchestrator file presence check based on enabled group permissions --- app/shared/user_groups.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/app/shared/user_groups.py b/app/shared/user_groups.py index 764cbca..444fd6b 100644 --- a/app/shared/user_groups.py +++ b/app/shared/user_groups.py @@ -94,20 +94,31 @@ class GroupPermissions(BaseModel): class GroupModel(BaseModel): description: str - orchestrator: str - orchestrator_sheet: str + orchestrator: str | None = None + orchestrator_sheet: str | None = None permissions: GroupPermissions @classmethod - @field_validator("orchestrator", "orchestrator_sheet", mode="before") + @field_validator("orchestrator", mode="before") def validate_orchestrator(cls, v): - if not os.path.exists(v): + # orchestrator is only needed if the group has archive_url permission + if cls.permissions.archive_url and not os.path.exists(v): + raise ValueError(f"Orchestrator file not found with this path: {v}") + return v + + @classmethod + @field_validator("orchestrator_sheet", mode="before") + def validate_orchestrator_sheet(cls, v): + # orchestrator_sheet is only needed if the group has archive_sheet permission + if cls.permissions.archive_sheet and not os.path.exists(v): raise ValueError(f"Orchestrator file not found with this path: {v}") return v @computed_field @property def service_account_email(self) -> str: + if self.orchestrator_sheet is None: + return "" if hasattr(self, "_service_account_email"): return self._service_account_email orch = yaml.safe_load(open(self.orchestrator_sheet)) From 6c42d7b447ef16c014b8261441f7d393dc7f595e Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:14:25 +0100 Subject: [PATCH 36/40] fixes test yaml with AA breaking changes --- app/tests/orchestration.test.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/tests/orchestration.test.yaml b/app/tests/orchestration.test.yaml index ef7ed27..9d2e44b 100644 --- a/app/tests/orchestration.test.yaml +++ b/app/tests/orchestration.test.yaml @@ -1,7 +1,8 @@ steps: - feeder: cli_feeder + feeders: + - cli_feeder archivers: # order matters - - youtubedl_archiver + - generic_extractor enrichers: - hash_enricher @@ -12,7 +13,7 @@ steps: - console_db configurations: - gsheet_feeder: + gsheet_feeder_db: service_account: "app/tests/fake_service_account.json" cli_feeder: urls: From c49cec1b6c0f4296f5c4e95cd7817d549ee8fee8 Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:14:43 +0100 Subject: [PATCH 37/40] updates worker to use AA 1.0.0 --- app/tests/worker/test_worker_main.py | 2 +- app/worker/main.py | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py index 865d039..d78b9ea 100644 --- a/app/tests/worker/test_worker_main.py +++ b/app/tests/worker/test_worker_main.py @@ -119,7 +119,7 @@ class TestCreateSheetTask: res = create_sheet_task(self.sheet.model_dump_json()) m_args.assert_called_once_with( - "interstellar", True, ["--gsheet_feeder.sheet_id", "123"] + "interstellar", True, ["--gsheet_feeder_db.sheet_id", "123"] ) m_orchestrator.return_value.setup.assert_called_once() m_orchestrator.return_value.feed.assert_called_once() diff --git a/app/worker/main.py b/app/worker/main.py index 7716b6b..9ce377f 100644 --- a/app/worker/main.py +++ b/app/worker/main.py @@ -25,10 +25,7 @@ Redis = get_redis() USER_GROUPS_FILENAME = settings.USER_GROUPS_FILENAME setup_celery_logger(celery) - -# TODO: these are temporary PATCHES for new aa's functionality -# logger.add("app/worker/worker_log.log", level="DEBUG") -logger.remove = lambda x: print(f"logger.remove({x})") +AA_LOGGER_ID = None # TODO: after release, as it requires updating past entries with sheet_id where tag @@ -41,14 +38,19 @@ logger.remove = lambda x: print(f"logger.remove({x})") retry_kwargs={"max_retries": 1}, ) def create_archive_task(self, archive_json: str): + global AA_LOGGER_ID archive = schemas.ArchiveCreate.model_validate_json(archive_json) # call auto-archiver args = get_orchestrator_args(archive.group_id, False, [archive.url]) + result = None try: orchestrator = ArchivingOrchestrator() + orchestrator.logger_id = AA_LOGGER_ID # ensure single logger orchestrator.setup(args) - result = next(orchestrator.feed()) + AA_LOGGER_ID = orchestrator.logger_id + for orch_res in orchestrator.feed(): + result = orch_res except SystemExit as e: log_error(e, "create_archive_task: SystemExit from AA") except Exception as e: @@ -68,6 +70,7 @@ def create_archive_task(self, archive_json: str): @celery.task(name="create_sheet_task", bind=True) def create_sheet_task(self, sheet_json: str): + global AA_LOGGER_ID sheet = schemas.SubmitSheet.model_validate_json(sheet_json) queue_name = (create_sheet_task.request.delivery_info or {}).get( "routing_key", "unknown" @@ -75,10 +78,12 @@ def create_sheet_task(self, sheet_json: str): logger.info(f"[queue={queue_name}] SHEET START {sheet=}") args = get_orchestrator_args( - sheet.group_id, True, ["--gsheet_feeder.sheet_id", sheet.sheet_id] + sheet.group_id, True, ["--gsheet_feeder_db.sheet_id", sheet.sheet_id] ) orchestrator = ArchivingOrchestrator() + orchestrator.logger_id = AA_LOGGER_ID # ensure single logger orchestrator.setup(args) + AA_LOGGER_ID = orchestrator.logger_id stats = {"archived": 0, "failed": 0, "errors": []} try: @@ -128,8 +133,7 @@ def create_sheet_task(self, sheet_json: str): def get_orchestrator_args( group_id: str, orchestrator_for_sheet: bool, cli_args: list = None ) -> list: - if cli_args is None: - cli_args = [] + cli_args.append("--logging.enabled=false") aa_configs = [] with get_db() as session: From a5f52ac97ca7dab0daa83833c696ad3866ae5954 Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:15:48 +0100 Subject: [PATCH 38/40] removes recurring log --- app/web/security.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/web/security.py b/app/web/security.py index ffdeee6..5b73997 100644 --- a/app/web/security.py +++ b/app/web/security.py @@ -84,8 +84,9 @@ def authenticate_user(access_token) -> (bool, str): if FIREBASE_OAUTH_ENABLED: try: return firebase_login_attempt(access_token) - except exceptions.FirebaseError as e: - logger.warning(f"Error verifying ID token: {str(e)[:80]}...") + except exceptions.FirebaseError: + # used a non-Firebase token, fallback to Google OAuth + pass # https://cloud.google.com/docs/authentication/token-types#access if not isinstance(access_token, str) or len(access_token) < 10: From 3b36936f7b01f86816dd66c2b93542b83d2458c2 Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 15:16:30 +0100 Subject: [PATCH 39/40] version bump --- app/web/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/web/config.py b/app/web/config.py index da25dcb..9f2e0fd 100644 --- a/app/web/config.py +++ b/app/web/config.py @@ -1,4 +1,4 @@ -VERSION = "0.9.4" +VERSION = "0.10.0" API_DESCRIPTION = """ #### API for the Auto-Archiver project, a tool to archive web pages and Google Sheets. From 65d63b377035b4597c83fca779cbfccb8d144bfb Mon Sep 17 00:00:00 2001 From: msramalho <19508417+msramalho@users.noreply.github.com> Date: Thu, 3 Apr 2025 19:41:31 +0100 Subject: [PATCH 40/40] extract AA CLI arg to constants --- app/shared/constants.py | 3 +++ app/tests/worker/test_worker_main.py | 4 ++-- app/worker/main.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/shared/constants.py b/app/shared/constants.py index 90a5067..dd334fa 100644 --- a/app/shared/constants.py +++ b/app/shared/constants.py @@ -2,3 +2,6 @@ STATUS_FAILURE = "FAILURE" STATUS_PENDING = "PENDING" STATUS_SUCCESS = "SUCCESS" + +# AA CLI CONFIGS +SHEET_ID = "--gsheet_feeder_db.sheet_id" diff --git a/app/tests/worker/test_worker_main.py b/app/tests/worker/test_worker_main.py index d78b9ea..39b2b17 100644 --- a/app/tests/worker/test_worker_main.py +++ b/app/tests/worker/test_worker_main.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest from auto_archiver.core import Media, Metadata -from app.shared import schemas +from app.shared import constants, schemas from app.shared.db import models from app.web.utils.misc import get_all_urls from app.worker.main import create_archive_task, create_sheet_task @@ -119,7 +119,7 @@ class TestCreateSheetTask: res = create_sheet_task(self.sheet.model_dump_json()) m_args.assert_called_once_with( - "interstellar", True, ["--gsheet_feeder_db.sheet_id", "123"] + "interstellar", True, [constants.SHEET_ID, "123"] ) m_orchestrator.return_value.setup.assert_called_once() m_orchestrator.return_value.feed.assert_called_once() diff --git a/app/worker/main.py b/app/worker/main.py index 9ce377f..ad93122 100644 --- a/app/worker/main.py +++ b/app/worker/main.py @@ -7,7 +7,7 @@ from celery.signals import task_failure from loguru import logger from sqlalchemy import exc -from app.shared import business_logic, schemas +from app.shared import business_logic, constants, schemas from app.shared.db import models, worker_crud from app.shared.db.database import get_db from app.shared.log import log_error @@ -78,7 +78,7 @@ def create_sheet_task(self, sheet_json: str): logger.info(f"[queue={queue_name}] SHEET START {sheet=}") args = get_orchestrator_args( - sheet.group_id, True, ["--gsheet_feeder_db.sheet_id", sheet.sheet_id] + sheet.group_id, True, [constants.SHEET_ID, sheet.sheet_id] ) orchestrator = ArchivingOrchestrator() orchestrator.logger_id = AA_LOGGER_ID # ensure single logger