diff --git a/.devcontainer/antigravity/devcontainer.json b/.devcontainer/antigravity/devcontainer.json index e5c5d67..ce1bf3e 100644 --- a/.devcontainer/antigravity/devcontainer.json +++ b/.devcontainer/antigravity/devcontainer.json @@ -94,12 +94,11 @@ "ms-python.autopep8", "ms-python.vscode-python-envs", "ms-python.pylint", + "ms-python.mypy-type-checker", "mhutchie.git-graph", "donjayamanne.githistory", "codezombiech.gitignore", - "github.copilot", "github.copilot-chat", - "matangover.mypy", "charliermarsh.ruff", "tim-koehler.helm-intellisense", "vadzimnestsiarenka.helm-template-preview-and-more" diff --git a/.devcontainer/vscode/devcontainer.json b/.devcontainer/vscode/devcontainer.json index 309795d..61b28b7 100644 --- a/.devcontainer/vscode/devcontainer.json +++ b/.devcontainer/vscode/devcontainer.json @@ -100,12 +100,11 @@ "ms-python.autopep8", "ms-python.vscode-python-envs", "ms-python.pylint", + "ms-python.mypy-type-checker", "mhutchie.git-graph", "donjayamanne.githistory", "codezombiech.gitignore", - "github.copilot", "github.copilot-chat", - "matangover.mypy", "charliermarsh.ruff", "tim-koehler.helm-intellisense", "vadzimnestsiarenka.helm-template-preview-and-more" diff --git a/.env.integration.enc b/.env.integration.enc index 97cacea..44a5280 100644 --- a/.env.integration.enc +++ b/.env.integration.enc @@ -1,17 +1,17 @@ { - "data": "ENC[AES256_GCM,data:uqozLcaDoElJ1C3+Lm31Z/YeAevk7/Iwk5cdzrUEiUNqgf59f8M3fxgcpd1IUI1MB6C17Eg7vn67fZ+X3+y8Dw5rmZXDg6oKqhOcYTeQfOoj1BsatazM62xGer0ppa7DSD72aA==,iv:2Q2XtwWzWzhQa9Xf9n3RVjeGzld2OVjVBzCA0m/3plo=,tag:TkyzfNqVM5T4GyT0lYEgIg==,type:str]", + "data": "ENC[AES256_GCM,data:jII7JxZJ8AfuJfVo+C/fFzSCr6WbZpZFuQaf2JrBMfNgx+k5EN3s/5A9Y6dk4NJj8ridB3asfUWiTFlkJRZtaR2whHj5IvSrUnUgrEiwFAumclkNrw7vN4jdva0P94D/VGKO4m3CJ3FxuLIHoXD4IAEXnLA3/NWJyf9ConzCsNcnqxovcCj4e+s68619OYP1WbTICMAOlo9Ugk+2+9j0qq6dIzMTrZU/7zm8M5CZkI7TwKYVhLuLo+9gWzmTY7+/c6MgFbsgpl7mK7BL7Q+2b2Fwl1AZa9wu3cicp+T/JwV2lyVgzg833hjiJDfinidWaOfYBuCvL4Mdlb930EAYTRgpOT6r9yVXEwOXNZb0GtyBL95ZCHYJKcElTGZDZJOvncPQ5MwmUppJMGw8pUTj9EoZqzT8/DM4JVUHTsMf9XDDDZhoSNm+u+jwFC4lQmSFBJ2660UsSjc2ydc3/nXgbgfgHheD94MBlpd12R779lEFK/8i8Ba7qnd8hVumwQPQKW/EOjm8K+ztQ0rE2S6P2Pc4xweu4mHrEugiYF1NYL4lD8QBMHiPb8/go0MLYe+gdrFQwDrCraZFV315NhLa3JZY4eiJqmDuUXTHxqVGZw7nFhuP2p1TL+r01DH0AHL3HR89cKxCsxmTYJSkP6J0iXoYzFWgQ32URDz6aiIDLAiGBwxzs24mWP7rbDhv0jPpwePahU4gXHjzLxsQyDjHNQLp/9v7N0FjWasEsw3LW0qy/BtdxC5ExlS8P9yI+PP9dpNWFIlnzOKDIrEK6nWr7ZM3xKwwIVp4XIoIQLfif8ga/BaTzFE4mLp+8bARLvOeyne0g5YdGGJHwvFTRdh+YsfW2ru1ogYyLiK3WPw4VqYFpVGTp8KoI3GzHyQqhCaf2RDYNuVDG8IJjbh/hoK4qpbkIf6NStifnikssjd2jPVOvsb6PxsjqQ238rMDRNNE56zD8Lfw7L5SQgQGLj5LSoZ7sWkxtswaeuWRxyHoeL/7wKteh4tphltWNy0eP+QzCTghDTw5At6knPrTkX2bn59VyYDgR4D/0uyhHujjHeyV8LnL7P/uyN16MuRqnpjsSzmbSQ6TJMiM8jOQTXaxnMwyz6W1jFW8Q67VfOzIOxBHS5U2nCBObU1MrNp2mAMPbY4j6ivLBN9L137K8kQhCmNEEEShbK41P/mhJg+uBD6xBKrC5WqoD/TQWHQQ9Z3g6NULzX3l8tZlhIOpk6nD9HqERDJWevQXcYf4br3LXFegMc1H0yC9tHYV1vDIXbodESyO8ho+XBLqfWotlHm2NfPhOh4Ybj1ZgxnESJVHRIJ2tDU7Jm4GW7l4QZquLWeXUMMXja1zZYCntQrq44FEdoVxJ3EAQ9mB5VIK3KR6ufp3PnA6bpmiDnzQYa7Vt0N8E3OjI+7Hik6l66UMuEQTMd1ui9A4wkt1PJuNIcK931UnjCKi+LZIM6mZnXnUD9EV7AevREFg0IUNjxKRprMY22rXXmF4O6LcnXMqCcuRa3+qdTWqtsOL+7laZCGiFUU/xOuj7pBei2oM1JxIozzY8s8rhpir2iIkXBcnoO9pQA779HNCxBldjtTyi4aFOaEjHqkYXBvHXndgcelPpgpwk/swsPdzT8PCsZJdYH+psSedy0e5x6+pv//xh9/h+WwWsAfyuXTjv/OysK8DDzLPkmwDSu10cS8xcZsxNVbYuYbjJRv6KQ4kSTQmrOqgzGpXTc3wrYJsAQdT8SzNyMi2AVbqK9IwFBKW/wd/wT1J25B02YntSlfI03THjnXPgE8y8EnoYoB2GW/OJE5UO+1kuUhLCzdBvapiK3mq3uMgn3TCMI44GMYkDH4AqcGH4f6/qwYivNcAApKRYoYAc6UEoo1PCdkg3A7BmftWGV2puLtY5RO2qf0VzXF2b4DPt+EzFhidYoHXKQzyknyLbdVqyORwmPvl3253HKFRv3aiei0vTPxd11qMPHIq+pO/kvlEPkFi2zDJVOf6o0hvaFGiYHvQBgrTwha+oeWy+PUh5AcdkgI/dQmatWOeatZb6aNwiBcauxyXtiK/2KOq1VTuCXEQbn0qyAY3t9bjeRtle0YVNLdTWCGPse1wjuZSpje8axQejeVvm/6gd3Dy3KlgsVEMCnFNhNx5vVLwLS/5Q8qA1jabLYv+eRJI8qjgj/i0m0Ty0JD4yMhw8Lmj+0vkgpL7t3eOyn3IikW95Q/Ay0aIbD4SR2HI+l8+RDhOThw4ZzrpIrM/BhyzagUaqSvyGAnMKRFNmLHJUbJUO2pFi5GvPa7488swaSKTfFOw0oI4wj87i3e0CmQHd7fFVB+INrUPIb4giIrIzDym+wF4TDEUK1kcWiAammd7eP3Zu066IWp71ji6akDZ1t+XQ5Wc8j20aBXSg3vPMB/ZtrDdR/VXPxQDCpjCuPyHJhd98ftZ+0ERJrbOZWZkBudHpv1+JaVxQraui2SfepztRWUOzNrdy9gD8EeQj4hRWZc5fo2f19X9pucANtN8sF2NjYQN57th4PaHH8zMgN50Ccob/RLLPs2gES1JqHRTKAxwe0udYciK3G1b0l4s4ZSTRZsjn+Sm++0FBg0gJz3JblbRiNHsCtZLlWab9IHYFhv3hnJ8mFaU0SbyadO4mjxCsaKqw+OaoFb/jR59nHRBoo71F3Xs1zzcuxgHaXm8C9nl0XHZ3fxGMfMVNKkmll6xe3SG7/NSyJdruJ3wB8A9N/NzSZrz5UKyiChwCcG6EfnZNmUvEeAyHs74s+tClAGNTiJHAj7QsaheoshO5z7eA9oavw7L/RXXShCmfkpfu/ss9p82EZ57NKCTI6uIV4ky0dO13uSTZArjKIAJrYG6Km1P7K2owYtYtNUia6XAI/KnjQAuWcbMEgdM/hQGpPKwhFgBUf1YgYiSdupLzi0eOz5+mMKdlkRJ1nFsafSds8wXQMvH9PA2Shyj2aulkzsnpFL5X5i5Kho53cjpm4k9fCO536MU40T08ORwHshQQged1XAjgPcM8bdREdMeMXfv1PcNyuoVZ9yF+m9eWJ09q/HG1l1JCEBxpBKHLtGmzmHddr6vgQiWgWHkAuZ17+vLuq4g6r39STuzSE6D0h8RkN7k3EQdIescdZr3iqq0ocgOqFndmKzCGFBZNWH89/LZvbXYZHXt5c+4EF2GY7AInNffymAGsjSofTf6y3DnNylGH9Sbl/RoqgfQoL5GXlmuNmtGqJT70PWm8WsVJoL+mckx5OLv5eVW97ItfIlL54frErhM1w+9lhFiyEoMhJrH4vuzlyAVvrwTKM0Fezq3607mRxMHL6uXJ3+9P6twMeoPnQRdaCFILCW5AoN37sZJAXDvFVSPsj6awNp4CMJdYb/V8gwIEuw/OFmgEWcSnCeY7+QvoM6okNp4yg+U/nPiz7hcOi5CWbDA3B4V3yb5sOhK/l/3xX34oHzzFYqtW/c5hcCaduiO9k31Wy1Y039TGa9yf6n9wf4j539sUWa2ZAyYhnBfmCyp1Q6Wv2ufwjZqx74kFVLbVmfTqqgssjwk/1KyxE8D2KagUt0pnL4qnzt3yZdlmhfbAEBE/RVUDsBQMhzGapushl1jySdkG725lMQcs+nCoFdxKplUr9Ka5arOiGwlI65XIfh0ZzuFd29STZ7mMqLMTJm6FDclYv5+lWN0WCYo/p3CURDq+28aK5hPaMZnYMYRZgR61Cp05ob8mDcqeDAAljCKgb77cpC35vKQNfJ8LDfMBEXTpoLPvzBaQ654wgzcALxQV/gp5d4RcWBM+BzGo5fkgZpUbRbAVmrcI/4i0nlt1CKiZUWH/8wcP+0fY+OBksWrieQq8G8KCk+W9gVazRfOTEGkiE245w2S+71pGhc6E8w9xCUkhPsXVqmWsAQHyIYkbBLET6un+XjErh0xOGznc2XE4rU27ZeQF6v8AEd2Ix7CQhtx+fIlg5+w8MSS4X/W54HReg6OzK69gIlj2Zer1jtCFxsDtAyJtdIZLhB/leUpbZN4K16BB8UogeQ9AM+zXROLzKGIcWIdTei6yqz4b06Ix8+Uc3zo3Ert4TYtmAY43WFxphUE4yNT6JkZyfHKngsRFR+adGvTm0rHI9k+ftG/6zZVQqgFqY/ocY1LlCA/uqF82+OAX2kWXooKeH1jH/7kzF2vow1jH9wVZ+iSL8c5kbnJrWly2R5Q2XZELr843BCoNuqd6xdyqdEWAq4cX82FPt0kXvV1cyoNY2zpJB4WBVC3lZb6F1kXLZNjJb8YSUn+0kFBXLjOeEenYWg0SpX6gc+8SwBfQz/z+SquNsNXA0pJ/uJ2FnZykm4X0rw+pseONx6G10AasLTX0wbchU5K7eDkl1FU0MpcqMhdtI1W+zD65gmSn5pQ/OWVQFgZTAQmhiLpLF66U352XX7db/1NdikRWQGDmxnS84mZ63HDpR7jKsoL4BwOORYVvPpq63FsJ5jaElySU3g2Q+I0VmcHXoPTV5x2AJZpnRZJeam2m8BDD8RKDOOgI69/IldKOnd8gqTNnLDg6mKlYTJW/lTyU52yqqP6nJjyLiVWiQssv5RVAvrX/WkBtilNMmnJiVyUHlsu87QBg96c/fJEmz8gh3pB,iv:a5ZAZ8sWFNO26CxwbHnClux1Ha1HyZytj3DwUoDanjM=,tag:AeQKLWvJKkebCIqzMjkMwQ==,type:str]", "sops": { - "lastmodified": "2026-01-06T12:31:47Z", - "mac": "ENC[AES256_GCM,data:Lj3bBLfsBt3Xpv9NiLQvFSO7VXo1MH1IBmV78asCIwf6smCpl1zgwuCkJyfnGRn02DT+OjrpFsYCtZ0NsGoMYhAtA1Ztq81cEmfqEmKeOdnpMlH2CB82W7Kk4MdeoKevUcrNxnO84ZtBDd3bKnwBRAyu4UDeeOZqJA6em+KMNxE=,iv:2FNikX0kGVp8JdH2merctM5XFL7patHtq0OtTsDt6jI=,tag:02yK5764SP/1BlazVoyICQ==,type:str]", + "lastmodified": "2026-03-18T16:08:54Z", + "mac": "ENC[AES256_GCM,data:tZktiCSC2x52QXIKXGitU9WV77qAzK5AvV3J+AROhmpomyM28bXw2XJLKVYj/5kRraumHXRCJnsV1F4bwQiqeOJW0XUyUP1erwmD9kG8yO5ggISzXWmK2fuSnhSaI4EON0NazxtKk+i63zgE+aOY3vnAdqkycbvc1xkCvSx3MXM=,iv:X9M4nwRAnQA32F3CkAGHCxOqOxpSNOQAYoYBtV+Zsw4=,tag:5otZTGkbco783kpfjsKMPg==,type:str]", "pgp": [ { - "created_at": "2026-01-06T12:31:47Z", - "enc": "-----BEGIN PGP MESSAGE-----\n\nhF4Df+t0WwSeCuMSAQdAraNGOzVhXDFVWZkr2HlmEGD0raVpOBzSUeBn6f7PfUsw\n9b4yepTfAYhU5hXj6XC1RDFK6iUs2cbSScFNbilFD84BbbmZ1WGgv/X9DhYewcE9\n0l4BtCMO7rhYDYOInEpBIHrZhApy8AipvKL/utJEBDBUGsrJZ0gD2d1ZRk6oW9zP\n8AR5vV3xLr2DaVQeZ3YPC94JKHUkRDgbp79BiXbU5hP17xIBqMjIUXa6e8Ghq7OD\n=6ADv\n-----END PGP MESSAGE-----", + "created_at": "2026-03-18T16:08:54Z", + "enc": "-----BEGIN PGP MESSAGE-----\n\nhF4Df+t0WwSeCuMSAQdAH7Bp/SMJIvD51M1rgdp0Vg+K7ldmBq86cWQxwqBhdQcw\ngpcUYdzpvF5JB390ROkc1mUrrfl5L9CL6nGlCqCQ20MI5qBlX3Jvlr2HVcK/j5aL\n0l4B1wv2Gccag5vvBjX/9Q9XpVO2ejZRzka7L051oQw5aZMQOmqj/vi6Yhd0FtBu\nqJryo1KKOqZWzHvqZ0bYYja9oZfTIR7mCqI1df6/9fGPZdpdH8J+G72IwyRgmul2\n=wIls\n-----END PGP MESSAGE-----", "fp": "37D38A6C0248214B007B6C5685E825F3377228D6" }, { - "created_at": "2026-01-06T12:31:47Z", - "enc": "-----BEGIN PGP MESSAGE-----\n\nhF4D5jdJleHfCY0SAQdANXQOT4vH5jfo9g+W5TYl9PRrUXB4LJ4UTQG08ttu0D0w\nIe1YvxnOXstMDPM7y6qPRxfaYQopqqHdrcVF5l+8+xilQ95u0OFUwEnGQhLnbZnX\n0l4BHYe0MdAt7Lefl/Zm3MUMwm9h2sYsi2xYDA2L45PRJbRZM5jleByRg+YEYnJN\nC48t4J21IQm/IHP1lU99P+q4LIb2rOiM5z8VSxopGluB7jdp/uOrpADdaTSSa9G6\n=IUVG\n-----END PGP MESSAGE-----", + "created_at": "2026-03-18T16:08:54Z", + "enc": "-----BEGIN PGP MESSAGE-----\n\nhF4D5jdJleHfCY0SAQdAbvs5ko+XMNfB++a0WZNX0+S1NE/BbeFO5P9v0FCaixgw\n5IkvY3GDkTlZcrNPpphIVzILBcY3LiVymlbAgL43/jZVoMjon89FIkpoQqV2wUT0\n0l4B3QSpNdDQb615oalHQkLL/gIkC0/gvefm1dK1Czl/aKmG55bVo8+8C7e3aMKw\nT39TTSvEI8/ZTZIi3qFun/Lp1+mFBfhlFB2rML4FTX+0fq6yJNaYuG/K6usiqo/n\n=QItZ\n-----END PGP MESSAGE-----", "fp": "CC7B10CE8D78010ABB043F8DB1C462E90012ECFE" } ], diff --git a/.github/workflows/python-quality.yml b/.github/workflows/python-quality.yml index 88d0ec5..306b0ed 100644 --- a/.github/workflows/python-quality.yml +++ b/.github/workflows/python-quality.yml @@ -44,7 +44,7 @@ jobs: - name: Type checking with mypy run: | - uv run mypy middleware/ --ignore-missing-imports --strict-optional + uv run mypy middleware/ continue-on-error: false - name: Security check with bandit diff --git a/.gitignore b/.gitignore index 90ad44b..4093349 100644 --- a/.gitignore +++ b/.gitignore @@ -217,7 +217,7 @@ scratch/ # Cryptographic keys and certificates for testing that are not meant to be committed helmchart/**/*.crt -helmchart/**/*.key +**/*.key helmchart/**/*.csr helmchart/**/*.srl helmchart/**/server.conf diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a7a4f52..12ed184 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: name: ruff format entry: uv run ruff format language: system - types: [python] + types_or: [python, markdown] args: [--check] # mypy - Type checking diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 4f7eff0..93272e3 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -16,9 +16,8 @@ "mhutchie.git-graph", "donjayamanne.githistory", "codezombiech.gitignore", - "github.copilot", "github.copilot-chat", - "matangover.mypy", + "ms-python.mypy-type-checker", "charliermarsh.ruff", "tim-koehler.helm-intellisense", "vadzimnestsiarenka.helm-template-preview-and-more", diff --git a/.vscode/launch.json b/.vscode/launch.json index 1ae5b80..236a314 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,10 +10,10 @@ "request": "launch", "program": "${workspaceFolder}/middleware/sql_to_arc/src/middleware/sql_to_arc/main.py", "console": "integratedTerminal", - "envFile": "${workspaceFolder}/dev_environment/.env", + "envFile": "${workspaceFolder}/.env", "args": [ "-c", - "${workspaceFolder}/dev_environment/config.yaml" + "${workspaceFolder}/dev_environment/debug_config.yaml" ] }, { diff --git a/.vscode/settings.json b/.vscode/settings.json index 5cf5c61..b6a1e9e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,9 @@ // For AI assistant instructions, see: copilot-instructions.md // For detailed project context, see: AGENTS.md - "python.testing.pytestArgs": [], + "python.testing.pytestArgs": [ + "middleware" + ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.testing.autoTestDiscoverOnSaveEnabled": true, @@ -20,12 +22,21 @@ "source.fixAll.ruff": "explicit" } }, + "[markdown]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true + }, "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", "sops-edit.onlyUseButtons": false, "sops-edit.tempFilePreExtension": "decrypted", - "mypy.dmypyExecutable": "${workspaceFolder}/.venv/bin/dmypy", + "mypy-type-checker.importStrategy": "fromEnvironment", + "mypy-type-checker.preferDaemon": true, + "mypy-type-checker.args": [ + "--config-file", + "${workspaceFolder}/pyproject.toml" + ], // Ruff Extension Settings - ensure consistency with script "ruff.configuration": "./pyproject.toml", diff --git a/dev_environment/FAIRagro.sql b/dev_environment/FAIRagro.sql index 70bdb00..1c8681e 100644 --- a/dev_environment/FAIRagro.sql +++ b/dev_environment/FAIRagro.sql @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae65a0777eb50fcaddd1ac884f95648d4aab558df4bbbd5d3603dacf33311cfb -size 252631809 +oid sha256:91371b6d2e35109594c9ab413de2a06d79a2431ae7c1e1fbf2cc721beb160568 +size 12289585 diff --git a/dev_environment/client.key b/dev_environment/client.key deleted file mode 100644 index 6c8c79e..0000000 --- a/dev_environment/client.key +++ /dev/null @@ -1,20 +0,0 @@ -{ - "data": "ENC[AES256_GCM,data:7MbW58hY3v1eJ/KhLHjxu/5LNbFlYRuyKU16EZ/g+2GgNXierh88dzdreajOsD9bWAYo4bP1gdHOuD8CZAqgVxnQji51K3IsrC9ExKoWY2f/KUkiqZ4w+weZq9mFybgZIqN1OorLR597XYRIU9UA1eDmqrDcVoY7cXAp3ahqZ0JFMNuASu8eLxlUy/n1acMax9k5gok9Ja2L6TAMMKE9OVSYrOs16DjjxbN3nOzYeDeJS4oYEkSVxbDbb38CFyXg9AWZbu3qX1u2Lz2YRq1SMGmR909bLU/prV3HoG/oVLc2aBK/4+laPf7dDX5YL61HYnyOIaL3qLQoKonpa3VJ8dG1zGMb1FlY5wtp6MWxf0wVzydN4qyqTDJ6T+hKOSvIoD94j2HVJSozB8NhqmAhB6x5YjKE+pRF1KgPT38D3qghPC0ZQHW1LqZxnwNYmZQBZv6A8XpsB5/Lx2GCubLHDiMcsJ1J52EeYb0CXDhp5GD2kcZvFYWeeIcMUR7pffK5mUV+les0/5pWAZi7fL08PRP8QnKsbKaN/JjAWef8RYfHlRX+JQWvEAuHTR0K30r+ywVrySf5kTyYfordqbCAGkNuKtGfDIa+rcciNSiBu5VHqo9AtRQ+M/GnDOFhkBIaUkVICZrdeNUxfIx3Qi6RUON5OLZNlxPUEicC12kKyxmNr4j5EJ09YWGWVrCLp+e0MXuvMkldpCDxUWxR/q0TwK7g3kQCiXhMAAxOX62RlIYQqmEmNGtCRUrM3Toq7n8ymQcgYb0N80mHvNmS6kGsd+ebCQNNgFJGiQRyrZanuPvaZx9MsXDkutsk6mT+v8Cn9cL+QzDa1Xul+G1TTCvWyVgMs6l34gf9rmyFb8IaxA/0P64yYcAtD3hp5JsPzVKD1JhklgS77sWXSj6b1Yj2APe7Ps4PdxKuYa+u5kGtZC56KsfULwmGtHmXEY+4eyKgSPlcmmDlsaiYTwemXUCTN1lSzJoGwA1Eo88+tRfNXcAC3/bhNrCiGj9/p4BNbG5EkbgjAdAjgmPS1VT2QBVQtMTZB4XywO/1ZLpXmZHQQCW7HSMGU/FsP9E4n8olwGGv/KlJ7S137SLmWUdFTQo9v5+oxL+zzhIWcMPAegMobqmzLKz0BdJGKTI3rYKbR+RmWBEUEnj83Lt9UHw76nuZSM7/jCEzXrAQOBEYoZHYe8Mejc12Y820Vhg5AThWJWKF8Dui/iFOwEhhX4UmPqXzyqWE0qIzZAMlx3iwq/b8SJoFelIvlgdKWzZuu9BPkLECdspE1mjvliFuJxIlgC0LGIUFneIikrufm6uOOyY+ZjhfQkMUePMDrgcnVHVLHkmVMqJ/mBbSlswWlu0nQc+AQdstqtHRfspI5pKOj5nKdHoBjalQopR1Ev3nYDm/h3+xeH5FDjQLZfOfuz1J4OKaEDottaHWKzQ80Ji7tgXlpXFE/Q7GzdqQgZVCObPbLSeVXlPJTMBgE2/yCfXdW+4dnh5Pk6zRibcb7qAQCtvjvKpiT8aeO01ZmH9w2l9xkbbukNzeeKg429tioO7aGRDrRkgGaeRlSTEYnKrzgJO4jZfjlQ0CCdJ0RqA5RpgrdNILKDIb+imDvWyaX5EZChW74xHe9laACVuo87MX1B4r746cynmlPvZG2y78Q+Hog96cNHEtZzwElfeUN2OTBzHVA+x+7+CPog118cn+mNMVJJqn/qej2rio4RRjCiJT/a99yWCUkbryTtqXV44hVwL/I5PdU9iW8p08uaWwtIkRw6nKRc71R4Z25y1l4YnlIiB4vh9/Ohgd5GNCqllX/k6TqQy+nBmbH4//va+fdOfsjc6AKyhL/2eKCkoymab7ZorYWkdw9/d7t0a5x5CrmUGa4q5onsX7Ogoqed4YiRsyDYwlZt81zlkVPcTSNViseeF9V1R7yneuM0mcdcsEgFKiwhjODmUvZ7eMGzkknHqSTJIcecjNAqHOXvA+EGh8c726XtfgpxSCL185OrBrcAqqokK4/VLEw64a5HRc7HJpxQeV4psaq9MT7Hme00rWhHMenJUOMNUcay1FP8hzY/xaHdEjJvV16IcquxIAMi3R3PBeH9fx6mYsyeuPVydD9GvTMsPmAHVlRFXL6GTRGJIRPeDOyqGi0HGLXB2MV/SCyMCU6IanLxJAEexJ0BT9fIg157BQ9RlRO16mCUXZ71JHcNkFths9SnI4aZSmIpiRXjH2RKtjYrkyOlLlArK8fp9o5k5TG+XXFcHS2QDh+nXhIwWn2kZv46oCh4GYEPaSXXYOH5ReHCmNRxTV5KZBgEBhf+sJ/fiUq+sCqcaoGDGTb5li4k80K9ef5hc8yQfr7M3oDVBeQmGDjvikLz1j8vLxG5RjOGZBmAOEqEllzp+WWLwqVNVorI3hpElm03cpAVvGTm5kUoz61gYW4d0DhDgUd9zIMp7TS2vq2V4DmJUz/+aBwzM8dCaRPn+BKwsH9YzrKVCuUigm1/+GjV+C42rOhOpKoNCFzjXRzRmFlESpud8AeRFbKJHXxeaMDdWLwUsWOjZXm1katWUBes6ymYEDILgaykqfQFMfIQrqCmAF7gFALJP9BEjknjiIG8tYezqJTE+sxKrSFqTs9aM7SNr+TOjZoazh660R0Reeipp2B1R81cglDXAG84ZoswRHFPb+xTEyGjqwKvuOGY5/KKrl6lVrnbKd9ZHvYWPqHNm+ROdWivgoOrRyeTYCs6kJudA2txvEBqowA6r7tm69bE/2luAxHzEvIxYagyB2QknmHUfl/FuUBbYimKHtDqqgW+bJZdNaZs5XQS0wnEPNxtN907TVhUT1Qikd4BWsuAyLxjgqfUQ2mWULrKhcdxWkAP4/3sMLvbHCgs5KWVdET6TIBIxHBrBGs1HLaSxt0WLnbl+ZxcXQUYRvdSRcPCrR4rviZjBqGe7t2dKLSdgAd7sHDm78bIaMa8UraLqwYxBDnx9AjPLXh3oiys3gdJ1jMSwfZDS4cSulhdhzj4Ivd8DUDgkC7kXEwI+bFhyvQoYWAbwpxPVain20gqH+mer1A8l9ZSi2O2zq8F0ImEYHA5ID8Xrk9AJCqgT+Q3j2foYlR9hxW/RVCRCzyAF5Kc90BpeNYndtvYzCF5x0bBB5kOP0ErDNQgLPOdi84LGByRNHcRkSA8NsWsWkfJo7ibPn6rtT3XLfRZ1YyxsWksbxqQmAlCHHFhTloTiGQvu0mcr3Mp0iD26aydAipLdNTkPtFk5giDAz+ZJroUis9S/6m4tUtiq2YAPknM/DL+0vatI9huHkGTkI0XJMLvhCqx/Q1LEG9LQEBGhSwfNPZOANkIJD7ORlbUK6s3UGg3XmpOCkhx5wo+caopF6ZX3xhRksaK7igPzfcSDgQ9L0xF6gH+tbpvTqU7hCxVs4XoUV2QpcCyx4VAm3CcRrQTRJ4iG2HggKwBh2x096G6UfG1pSqD43j0/db0gRBvmJdzYV1yacaFwOFTjrvTd1J0n1dv+5LHhSbtKcMmiGekJbGyKZeNFjnfJ7R+9NYwlWH2gialRJnwVnJKDaEhzX2f/zrkuT9ECoigCPj97HnuX5b8Jh/Vg358Rb3RPUegMHS4piNJl98W98xCGPTr31EB9lkw45+iVxegfyas/U48lyfsD/GQ83Zp5fDS2ZL6VljBfnXn5XdnmwXX1LN5Py739AKUX9yVbtjsgauUwmy+L7teJ6uzSJCAvw3GC9alQYSlxSvSsLlx9FEklbaCrtCpFKri4CXzRc46RhXXiHEO4ww4zL7oofXQbHn0hDXjUBovq+A6nq1bpodtOXnudGGOsWM08Zx23gtJNkop6Anix2R0t1k5pWIdx5aKfV7z2MNBwzmF8nkqgIlKjn6aZjnIg3Lli7e9yPYeDMpwAr3zodXU9Isq7WjVxvdjs415tJ4LBHgLm2bw3CydekgR3dC464ygklEeb5kzv/8Byd2zw0zg9++2NjyV0cJGfP8SWzS5eMrxKNHtVWjpmAi6wnocJxRCabQKcz8tOXZGsijvU6adua2f0QsuUtTeIlMxCM7TizQyM5DMTWlN4odXrvUFo56AG8zSQxDIyM9zZF4A3/jg2HEjcSjP9FzTAjMC2gAbtttU5sFwiOqcwRLhTwvcfzJZscS9LK9jBJ0rcfyNnaCP/Z7MhcKlAauBidnYr0coFgU+S9PQFKOjFiQs4kEUP/PIrwfGmlmKgqCHqBeqfN/E6HYpHdX7k1F+x0vXa6iFuEu2jkg2KGhSBs8KD+E2uxaNvBKdWftrYEfwIc7ckqakhlYprm3mMtPfRApphiSSdbyp1hbWnYUm/32awXf4tS,iv:2qFwj2Nsml3H0tiMXuD6l7uY9okPDT5y7zWuSrHVr+E=,tag:cEwtJUX7J/d3Lr3WEijVcw==,type:str]", - "sops": { - "lastmodified": "2025-12-02T12:53:24Z", - "mac": "ENC[AES256_GCM,data:AAl1APDEL/bJElJzi0T7Z0tDURLTzsjLu+A2oCb/nDcz+QtHZPyqvs0hZJjel7xTEfQ6BrXBwynVcyg4EeH/HIODMSdzrDGUm23wSUG2SSGytMsARQ3TacrXrOKBPLpCGQUqo6SrhtyUqQBNJcZt9YtJxhhFMWkE0AzI0v8RWo0=,iv:5rWrV5Se3AQzdTIYeyjh17mA3nwlA9Xp4oUFfhvwv3w=,tag:KOeiQ1Cj3oAjdv9Ju9u2/w==,type:str]", - "pgp": [ - { - "created_at": "2025-12-02T12:53:24Z", - "enc": "-----BEGIN PGP MESSAGE-----\n\nhF4Df+t0WwSeCuMSAQdA0dkbsnFHwcNL7i+FjE48l3kQ0MVpoHTmUpE9B+Qgdlsw\ncj/2ZsQyGzz7VAPkJcxQw6NMnQ8wYL7g9PAGUykad+FhVSNfuC13c6HCdhK8+T6/\n0l4BUUbo4F9xuoIwqgISyPo9qM3+bjLNeqocpPf0LtJG9s1MckX7jB3Hcg5ovSg3\n/bR3EXYAjL4wlo/9D9PLm9z47WhuDWT2C9wfRMP4u1t6cuJ6Hor4sdq3nURivJrb\n=zBR1\n-----END PGP MESSAGE-----", - "fp": "37D38A6C0248214B007B6C5685E825F3377228D6" - }, - { - "created_at": "2025-12-02T12:53:24Z", - "enc": "-----BEGIN PGP MESSAGE-----\n\nhF4D5jdJleHfCY0SAQdA+8HAqzLzJUVQi0puNWnD1QUcRMm1f1KLeYyQLuiOtFcw\nO99FcCc+6Cgp6R99U5q7Q1o1Zr3w/aLzohtU8KSK6YS8iTOUVcPJPhDq4svCbMO0\n0l4BPrveRtH6TDkfC2UhRdSoiup5jceXWZ2ZDalv3v5XAlmB56wk0X3i0g4n7B7O\nsD48AmqjV4JvS/uLK/aaB7tq4JIifTpqVX9h52B30YUhO8QPYPmqKOCi1or13Vip\n=DRhz\n-----END PGP MESSAGE-----", - "fp": "CC7B10CE8D78010ABB043F8DB1C462E90012ECFE" - } - ], - "version": "3.11.0" - } -} diff --git a/dev_environment/compose.yaml b/dev_environment/compose.yaml index 6d61fa9..4a1e502 100644 --- a/dev_environment/compose.yaml +++ b/dev_environment/compose.yaml @@ -3,8 +3,8 @@ services: image: postgres:15 restart: unless-stopped environment: - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: postgres ports: - "5432:5432" @@ -24,9 +24,11 @@ services: condition: service_healthy environment: PGHOST: postgres - PGUSER: ${POSTGRES_USER:-postgres} - PGPASSWORD: ${POSTGRES_PASSWORD:-postgres} + PGUSER: ${POSTGRES_USER} + PGPASSWORD: ${POSTGRES_PASSWORD} PGDATABASE: postgres + volumes: + - ./FAIRagro.sql:/tmp/FAIRagro.sql:ro entrypoint: /bin/bash command: - -c @@ -39,9 +41,17 @@ services: psql -c "DROP DATABASE IF EXISTS edaphobase;" psql -c "CREATE DATABASE edaphobase;" - echo "Downloading and importing Edaphobase dump..." - wget -q -O - https://repo.edaphobase.org/rep/dumps/FAIRagro.sql | \ - PGDATABASE=edaphobase psql + echo "Attempting to download Edaphobase dump..." + if wget -q -O /tmp/downloaded_FAIRagro.sql https://repo.edaphobase.org/rep/dumps/FAIRagro.sql; then + echo "Importing downloaded dump..." + PGDATABASE=edaphobase psql < /tmp/downloaded_FAIRagro.sql + elif [ -f /tmp/FAIRagro.sql ]; then + echo "Download failed. Importing local FAIRagro.sql fallback..." + PGDATABASE=edaphobase psql < /tmp/FAIRagro.sql + else + echo "Error: Could not download dump and no local fallback found." + exit 1 + fi echo "Database initialization complete." @@ -54,15 +64,15 @@ services: db-init: condition: service_completed_successfully environment: - SQL_TO_ARC_DB_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - CLIENT_KEY_DATA: ${data} + SQL_TO_ARC_CONNECTION_STRING: ${CONNECTION_STRING} + SQL_TO_ARC_CLIENT_KEY_DATA: ${CLIENT_KEY} tmpfs: - /run/secrets:mode=1777 volumes: - ./config.yaml:/etc/sql_to_arc/config.yaml:ro - ./client.crt:/etc/sql_to_arc/client.crt:ro command: > - sh -c "printf '%s' \"$$CLIENT_KEY_DATA\" > /run/secrets/client.key && + sh -c "printf '%s' \"$$SQL_TO_ARC_CLIENT_KEY_DATA\" > /run/secrets/client.key && /middleware/sql_to_arc/sql_to_arc -c /etc/sql_to_arc/config.yaml" restart: no diff --git a/dev_environment/config.yaml b/dev_environment/config.yaml index 4f21934..4f6b1c7 100644 --- a/dev_environment/config.yaml +++ b/dev_environment/config.yaml @@ -1,17 +1,10 @@ log_level: INFO -db_name: edaphobase -db_user: postgres -db_password: ~ -db_host: postgres # Docker service name -db_port: 5432 - rdi: edaphobase rdi_url: https://edaphobase.org max_concurrent_arc_builds: 12 api_client: - # NOTE: Change this to the external Middleware API URL api_url: "https://middleware.fairagro.net" timeout: 600 client_cert_path: "/etc/sql_to_arc/client.crt" diff --git a/dev_environment/debug_config.yaml b/dev_environment/debug_config.yaml new file mode 100644 index 0000000..2c309ce --- /dev/null +++ b/dev_environment/debug_config.yaml @@ -0,0 +1,16 @@ +log_level: DEBUG + +rdi: edaphobase +rdi_url: https://edaphobase.org +max_concurrent_arc_builds: 12 + +api_client: + # NOTE: Change this to the external Middleware API URL + api_url: "https://middleware.fairagro.net" + timeout: 600 + client_cert_path: "dev_environment/client.crt" + client_key_path: "dev_environment/client.key" + verify_ssl: true + +otel: + log_console_spans: false diff --git a/dev_environment/secrets.enc.yaml b/dev_environment/secrets.enc.yaml new file mode 100644 index 0000000..9a3de88 --- /dev/null +++ b/dev_environment/secrets.enc.yaml @@ -0,0 +1,32 @@ +POSTGRES_USER: ENC[AES256_GCM,data:1snx+IWLtpY=,iv:RJmLQNw4LeWnk6cHreQ2NJGH6CCxCHwV4HTwVBhURy0=,tag:6sVA0Q/HfOBTVX1unqyM7g==,type:str] +POSTGRES_PASSWORD: ENC[AES256_GCM,data:XpPBfwMRbx8=,iv:6EzeruZCNqIr4c3g+5VFNv0Kg5RuTABeO7KGIvWWUm8=,tag:iRh5q74avHedeJOY64b9Tg==,type:str] +CONNECTION_STRING: ENC[AES256_GCM,data:x4PUGfA8DOVB5eJ4NXQ4poZ/CosXaF6U40n3jvx5P9I0Ud0dvYv6N+ghlI4sTg3Yokq3Weh7kw==,iv:COsAN1zvX9Vg3P0sVpsaDIdAUHlpazdHRydrLf1JLjY=,tag:iLC3oxkPzWKzZHd9ULKuiA==,type:str] +CLIENT_KEY: ENC[AES256_GCM,data:/f3GCdBoHS3UgsaQKOqTTBJb4ueIWhBrJTdrKuAFv+d4tav4WM742YTm2zeY2laiy2UOGhokPuguDFY2pxs8QuQC9/2wimNCNjdiA1RGpc5DPYj8CLBmFofBjXyRtppzSsAKUANKKFwzlw1hKDcvUqBdH+I1c6buNP/Q1jlmCej3eOac3jjxQKVW6vixavHPMFsaGQ355Qa/piPLOYFdqTxQ0rpLMe0CgDgCqP58eUcLXo8XT/Lg7Z8wcnMNvp4l7ikU+8y+DGBknxFG6kf/tS2arBCDTX054Yt+FkB0QLOV0bVQwJWDMZlnfOkbOEHmKC1nWEg/XQhIBNWE5ym9+OBi1fWZmKzy8Au/Etk+NS6LKEHRG9RcSo4SCuaTo/Z+ScBabGJntQtk6m/ISHVMWXwZmSvSqYFi2SSW98/s0uufybBTSSF1p7RS6NaR/R5zujSpBM0CHL3sdVE1wSnh2MmlIfLAEJHBpXcXnX0G9ScXSDM8MBJYgvbmHAOn1HVjQNBw2RfJEGg8khSZo3EU+nIzdAlDJ+qw/lQrL2TTLrkc6P0Hajr9/O9Kk8MTtemlho82bYdgq0ySOPq9If+IJzyngSy4XVaj3H5b4pNbdAZ+yGitGAI6hmVwBjPYuYJfH4noZS4Amf6JjuZjnZvYaRAnvF8CL+ZjRUqSdKOFV92UjFg/lhRg7ufQY0g89uXTZL75nfYnQ3F0myXkXrEbLLjTUhaJMJDGlzTrt1rG2lMB9/swU8jNTuHC+bjZ/A1Zvza9LZ9wWyLpRVhgqFC64JFarLtBgkuzDacA9LpaaXzOxeQXlIhNL0cwtIvn/CVrsPGGTjm8asL622HOlYncCNe98HhJ1I1x8JxsG3VVudsf/8BCqOIbK0kxSQ1OAdZp9Q7D8u8qxQoa5vFOYYd4G2d2aw3KbwKvjSjXC2oR2IIP/283prHv1s9Ok8cph9uCZtwF/SULIMNJfD1qV5MyqVFEcGACuOC00TSCQlhw92kHYxWIFv4ZQkEf188ZtgR55Cj/f5g1Z2IuaCwhTnd41y70nBt+yw3YF2tmltSavOhUgEntfUtEnagXBXEvIaG32+3DpKY2IByKESCQpuLEBU9Q41A/1d0Ga5WoQ1g/5dYpPGnm/tX1uaGAGly2ROsXz839ec4xDXsqcBDr72Ac2y/TTOvviZiPDo0SvNnus5xp2KTAhjjEYWLPHGGIJeu3qQ7+u5aVjZyhzF+L8OfFDa65pOjO0XvtU/XGy7TFCojkduE8jGUODJHaGo/+fLE9EVKfM1RqNL+KzGpjj14Mv/KIbIJC8mr8dJhpxNIOxMoSPDyUYSzvYy6AAY3Oq4gTMMW1A4Biyq/YWJGVdHGjz88v0JCgb2wdP2v6DOuExAcobfatWh4c2MHpTzWtr0AdR00xtqNkhLjFrm7zS6qDL+yjfHwO2pOKjHINh/ebsgyP/NhjxyK0ePTfIPaXjOcjIiHaNVdnw6UR6GYc0Le0iMZMTpCk+ElaivJ8fwTpErohqH61//d6mrqjt6OqzboFvEISZ5hvgmUNuAF86vRWr65RJr5o9DXMGKi2kWLAOL7sOjTHaP7zPyhU86+tXPWBFKmOhhufhGMQ+pkrcTO8kqAi359wLc8nnOxj8UlUSDn9MA+pLOgR+Vqy45LVhLEcQB0hU49irESREodC47j3b1DacguKkIKhp4+YU+1xmWuTvm0JDh4sFNiZ5PsqQl6KwZQUoGZLEq4zwuB2WK0Vgc0Ta6YRjPV18ZFzp9BFD/PVqnVVywt6wD2+b0bcbeWuYdKP9p0WQkKs+elK04diw9Ums6AiNsnG8ltMAAeGDqmln4DIIW4dwJ0uu0gPIbOf3B2wfQEJqmDqi0BfT0RtUChqym4AoXmQiF3cGC2fGIh3zXZu5erGot2hdfh1E4l2UPEpQO4LhDf9Xpwb6JSa23J+dJGei+y1pe7UREicDsGF1d40t6j4GcDphFGK5ejwAOmPOQge818XffR/cn/24DZSJXU1612MOKcmIbX7TD9PqYvWee7Cn/MIe7agr8ER7oUDpPjzaVw2VqVkQBX5iFhDeALJrJkb0wGvWNWihn/gwe3NNgVf/xmPNO9UkfxJPfEUEG7LyPXbx5+lYxCqHddskZRIbTn3Qpmi0BFl3c6l2X+L6KjFrSMHE0L0BzZ7dx8Wz625o6fJIiOaCTgbENOLm7rvqs5AF3fYnb4CWeD2/sySbCkD7+X1THdBrsHkl9JoiRjdIZHswcrH+W5eHvGLyC2X9fAaXmg9ODbEMuPYHUjEQDIHnLt2u3O4TI2Kobcwu8ExIChkjnKUp3atu+snPyhMz0nHXdCRsRmmQcZG0zev6yJ9BBI+qFXiWI2lAhvymKhjC68GtOk9lF0y+NpNsSn1RaYH+qrfUQMF862cTsk1ZrWzhXKjibX/sk/amhhd71HUG7x0lNeIeuqyOGjFa97rHLAtdzE3sjaG+ICXJV1atwmXB5+eShRpqkXZOfkeL39U7wlvBz0uqFpcwqNH3ikSaCPue50/70m8NneK64Zu0DtjvvxH7PFrhMxsTFmLg0UizLqyqr2o+n9CFMYlrTyd/E0Vpz96Nsho0ACRQRKewhjqanMp5oe7D/d3wVd8QP9uqWlk7a8o+gV7sthMvaG81zbG5sPxkM9vHSU1Ys3UodLFYA7GEQ/e3sbaiNmh51ZvMgdCPQYbOmjbwWU0HYP6EVPQgUQ0QcrSaw+ES6PVk62kDuejGkX4SDjVSClV63RhcduXbQIW7ww2y51nDMkigvIy/01h+ViotiQjYyX1HDJUa1cB6nJsV/nq8lUeqT6ZKqwOXNqHLXRoq8QqC2+ZHt8wiO5OEewxag9KIu2E2BPJR5yvNMNcVcX8a29oi8R/m3dGUxoPaz4bnl+hXm06FacZMtq83MLWFjA0K6KVbUJDRAF+MA2NrKS6S6SMrCP76jmd7W1/8QyWIZUmZPto77kgvwGetMfm/FeT5WXb3MiDUAcTGPAh/FuCM+NjJ5ICwalStUYgXT1+cVMCizH/BlIJyd77o8CnZ4VBSmu8/atK/mpkQKj1MREkyF0ex9LFn+iyxOFMM6OeLdaHwStwqQU4j/cQUlPnGqnpDhwj5Z26CHT6s4+gCWHQF5EZx0qCyvl3zcPoI0VrNngm/rX/qZuPAruArAfgqtkbtdM2URNvMPvZMzgwIF0WXVqLjhnVtF4GAciUS3OS6uMJcqyl1D6BSBcaU1kOHUibC81ILTyAGyLb89F+BOmGFXbpoOOjy+DiNAvhpM0ZH+x48YzsVxG/ia89xxNpaZnswg9UOoJGIyW0BGJno/zKSp1Vu73nurYP5Mc9bK/Cvht7b8OG2YsJoLsoU/TbxFKWsfciYNRyzJjbQl/cl9rh8bMSz/XX/NZssKtb5QD4CJRVAFDQn/TC9k7fJXMSLd28cfzXwZk8w9Pp1oV/YbSdCLjcfyD9Zbxi/mA5hVCc+Z9oeLpSOEMz1OdckX9fuITMYvpp2mNkIuhfRT31kKKjFhC3w/H8Il0eULSIQ43mg9ano8rfHR1uwShXgVkpFT3q7aCbtQlf9w1Xn554oZ81Tao2XzTcC4uJq/W+ssrHd479+cJwcIxMoqtdHvIiG/idchmxzI7mLZOS2kEBlARiC6BE/AXtHUu4GNpGxLSBqcyz8tbgVbqYdwjmE6OPLLM1bYYPy9dBVFSvj1iYRM2bPLGiY23XsMmsb20GUOAvsocO7lXotdwZP+7P43o/SrKikszZXpdR5z8Jmc6r+ON86blu/Uz5lrxFn521NYnXDLAc1wykyKVKDX3DpjxRRWzGWOtP2QO1XdcV3iB38quRbsivESw5uVESnGyFZ7gTPKIcz6sXif1D1yOh1Ama/7116z5Eoq+lneMf3O8SpCvAl7TBWZX74vg6E93Ts9N13nsbzD6cb4jdeqMlfgHjqhASvpU0548mYMDJl2wGjMlj/hF6ykJZHzdwtFfMXdyRPAJedakTSwncrGSANTc8eE7dMyXZicx8Qu2/Pf49OVJU40zCPuVAmyxBZJ+Xw0wuUiQxYNBIiNQvZCsMI3ZAQw1mPDUFyLUyDHk7cMtYEO6FHy5g1880nelHUWzdInZ5qeFne/0MDyWySoO1TjoSJGiidPsphcmXUhRi7H/I1F4TZUl4GhEdQ/TLu/ZGtMT//FrpWNtSgFB3dwzWgqFI/CdP6hT2eKi3JAoUTOOtjh5Y7W0cJUqj/uUu6+UhxIWKGPUS++WeKCHd3rCJ5M47bm3Cw+jWOwtpPzLn1qPDTlOfHr28jfbZ2I2CVDAOsRbMpCXp4TEm25LHPf89,iv:mCHQ+8AvEQfmAl3gVRqRYjFBLEGZcwg83vDer0y9TSU=,tag:+yj0e1eorei7cYOT8d2YRQ==,type:str] +sops: + lastmodified: "2026-03-31T08:46:14Z" + mac: ENC[AES256_GCM,data:dz9qV8pTzDax8meTQxKRetmJPBx78e0yZhPqSsAXcdR/x6LX91wf0E3w1NaK+0fre8jRnwvIiVRDKTnK9eq1kmrfLYPjDk6K722ZT2MS7TPBVeBLoSCFkIsrDA0EnhyQtePErH1HXU8IEkNMYfC+lf10YvlCmmHraW2k/n9ZAyc=,iv:BL1Xg5JVIZxmLT/tCTPLJ/cCRZHiR/zikQdd+2qpWbI=,tag:uvxPH4lTyCxgSQEr25nYYA==,type:str] + pgp: + - created_at: "2026-03-31T08:46:14Z" + enc: |- + -----BEGIN PGP MESSAGE----- + + hF4Df+t0WwSeCuMSAQdAl9L6loTkOYK3ZDhI6rDC25GxFAlsg/x6VCeyuBXHm04w + HR3hK16iNgtRPR3xNbrHi7udSjTS8FPUXA4i6BD0E2yskyVNekOm0FNSXytTqivv + 0l4B1G0AepeD8RGsjkNwZqAu3/dTT1u/gMM+Q8Q9Ik3uu6mO7bJsT0ceQ29aH+/5 + WuOe4e8mFPkPIjkahZ6UBHxPc0tC0x8dLh7cBY89+N7q9ZJ0EUcZFqCNf57PAp45 + =Qo4D + -----END PGP MESSAGE----- + fp: 37D38A6C0248214B007B6C5685E825F3377228D6 + - created_at: "2026-03-31T08:46:14Z" + enc: |- + -----BEGIN PGP MESSAGE----- + + hF4D5jdJleHfCY0SAQdAdmWPXym2Lf0xwEuPtS8E1Q4ReIX+tkhhuiZFSjlnoisw + qb539ABa7vR9s7i0QjDb9vg7w1hI6MdKFmBtaM+3X2UvkdittnrPr08R4bN/SQvk + 0l4B6lY+tt2K8bfUzkd0phpZK4/9qK2GymDoLMInfdHwFELlLk75ECHzNwF4E1Bt + ett/idrYxvoxHz0FKZ8uKo4VTrgMYt9HbCofHXi1NFiKUsQYeqSN5AOrz6ssvs24 + =o+hz + -----END PGP MESSAGE----- + fp: CC7B10CE8D78010ABB043F8DB1C462E90012ECFE + unencrypted_suffix: _unencrypted + version: 3.11.0 diff --git a/dev_environment/start.sh b/dev_environment/start.sh index c292575..2824879 100755 --- a/dev_environment/start.sh +++ b/dev_environment/start.sh @@ -22,17 +22,17 @@ echo "==> Starting SQL-to-ARC with EXTERNAL API..." echo " - Local PostgreSQL will be started" echo " - Database will be initialized with Edaphobase dump" echo " - SQL-to-ARC will connect to the API configured in config.yaml" -echo " - Using client certificates: client.crt, client.key" +echo " - Using client certificates: client.crt, secrets.enc.yaml" echo "" -if [[ ! -f "client.key" ]]; then - echo "ERROR: client.key not found. Please provide your client key." +if [[ ! -f "secrets.enc.yaml" ]]; then + echo "ERROR: secrets.enc.yaml not found. Please provide your secrets file." exit 1 fi -# Use sops exec-env to pass the decrypted key as an environment variable -# without writing it to a physical disk file. -sops exec-env "${script_dir}/client.key" \ +# Use sops exec-env to pass the decrypted secrets as environment variables +# without writing them to physical disk files. +sops exec-env "${script_dir}/secrets.enc.yaml" \ "docker compose -f compose.yaml up $BUILD_FLAG" echo "" diff --git a/docker/Dockerfile.sql_to_arc b/docker/Dockerfile.sql_to_arc index 7d02a2c..0340ce8 100644 --- a/docker/Dockerfile.sql_to_arc +++ b/docker/Dockerfile.sql_to_arc @@ -8,18 +8,16 @@ COPY pyproject.toml uv.lock README.md LICENSE ./ COPY middleware ./middleware # Upgrade pip and install uv -RUN pip install --no-cache-dir --upgrade pip==25.3 uv==0.9.27 +RUN pip install --no-cache-dir --upgrade pip==26.0.1 uv==0.11.2 # Declare build argument for versioning ARG APP_VERSION=0.0.0 +ENV SETUPTOOLS_SCM_PRETEND_VERSION=${APP_VERSION} +ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SQL_TO_ARC=${APP_VERSION} # We prefer to build a wheel for our package first. This ensures we have a clean, # distributable artifact that contains only the necessary files. -# We set HATCH_VCS_PRETEND_VERSION so hatch-vcs can work without .git folder. -# We also set SETUPTOOLS_SCM_PRETEND_VERSION for compatibility with related tools. -RUN HATCH_VCS_PRETEND_VERSION=${APP_VERSION#v} \ - SETUPTOOLS_SCM_PRETEND_VERSION=${APP_VERSION#v} \ - uv build --package sql_to_arc --wheel +RUN uv build --package sql_to_arc --wheel # ---- Binary Build Stage ---- @@ -31,16 +29,27 @@ RUN apk add --no-cache \ python3-dev=3.12.12-r0 \ libffi-dev=3.5.2-r0 \ openssl-dev=3.5.5-r0 \ - cargo=1.91.1-r0 \ - git=2.52.0-r0 + cargo=1.91.1-r1 \ + git=2.52.0-r0 \ + unixodbc-dev=2.3.14-r0 \ + curl=8.17.0-r1 + +# Pre-download Microsoft ODBC Driver 18 for Alpine (kept for runtime stage) +# Use -L to follow redirects +RUN curl -L -O https://download.microsoft.com/download/9dcab408-e0d4-4571-a81a-5a0951e3445f/msodbcsql18_18.6.1.1-1_amd64.apk WORKDIR /build # Install uv core tool -RUN pip install --no-cache-dir --upgrade pip==25.3 uv==0.9.27 +RUN pip install --no-cache-dir --upgrade pip==26.0.1 uv==0.11.2 # Declare build argument for versioning ARG APP_VERSION=0.0.0 +# Pretend version for both workspace members to satisfy hatch-vcs +ENV SETUPTOOLS_SCM_PRETEND_VERSION=${APP_VERSION} +ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_SQL_TO_ARC=${APP_VERSION} + + # Bring in the pre-built wheel and project metadata COPY --from=package-builder /build/dist/*.whl /tmp/wheels/ @@ -56,13 +65,14 @@ COPY middleware ./middleware # as pre-built wheels on PyPI. # 3. Thus, we use 'uv sync' to create a virtual environment (.venv) and resolve all # complex dependencies exactly as specified in the uv.lock. -RUN SETUPTOOLS_SCM_PRETEND_VERSION=${APP_VERSION#v} \ - uv sync --no-dev +# 4. We scope sync to the sql_to_arc workspace package so uv does not additionally +# build/install the root project (m4-2-sql-to-arc). +RUN uv sync --no-dev --package sql_to_arc -# 4. Install the application wheel. -# This replaces the editable install from 'uv sync' with the optimized wheel. -RUN SETUPTOOLS_SCM_PRETEND_VERSION=${APP_VERSION#v} \ - uv pip install /tmp/wheels/*.whl pyinstaller +# 4. Finally, for packages like sql_to_arc that exist both as a workspace dependency +# and as a pre-built wheel, we explicitly 'uv pip install' the wheel. This ensures +# we use our optimized, pre-built package instead of the 'editable' source install. +RUN uv pip install /tmp/wheels/*.whl pyinstaller # 5. FIX: Manually reinstall git dependencies using their INTERNAL names (underscore) # to ensure proper namespace installation. We do this AFTER installing the wheel @@ -90,6 +100,19 @@ FROM alpine:3.23.3 WORKDIR /middleware +# Install runtime dependencies for ODBC (MSSQL) and Oracle +# We copy the pre-downloaded Microsoft ODBC driver from the builder stage +# and install it using --allow-untrusted as the public key is not pre-installed in alpine. +COPY --from=binary-builder /msodbcsql18_18.6.1.1-1_amd64.apk /tmp/ + +RUN apk add --no-cache --upgrade \ + unixodbc=2.3.14-r0 \ + libstdc++=15.2.0-r2 \ + gcompat=1.1.0-r4 \ + zlib=1.3.2-r0 && \ + apk add --no-cache --allow-untrusted /tmp/msodbcsql18_18.6.1.1-1_amd64.apk && \ + rm /tmp/msodbcsql18_*.apk + # Create non-root user and group RUN addgroup -S sql_to_arc && \ adduser -S -H -G sql_to_arc sql_to_arc @@ -100,4 +123,4 @@ COPY --chown=sql_to_arc:sql_to_arc --from=binary-builder /build/dist/sql_to_arc USER sql_to_arc # Execute the binary inside the directory -CMD ["/middleware/sql_to_arc/sql_to_arc"] +CMD ["/middleware/sql_to_arc/sql_to_arc", "-c", "/etc/sql_to_arc/config.yaml"] diff --git a/middleware/sql_to_arc/README.md b/middleware/sql_to_arc/README.md index b2bcda7..78fbd20 100644 --- a/middleware/sql_to_arc/README.md +++ b/middleware/sql_to_arc/README.md @@ -4,10 +4,10 @@ The `sql_to_arc` package converts data from a PostgreSQL database schema into FA ## Features -- Async PostgreSQL access via `psycopg` (v3) -- Mapping of Investigations, Studies, Assays to ARCtrl models +- Async Database access via `sqlalchemy` (asyncio with asyncpg, aiosqlite, etc.) +- SQL View-based mapping of data to ARCtrl models - Batch upload to the Middleware API using `ApiClient` -- Pydantic-based configuration +- Pydantic-based configuration with generic Connection String support ## Requirements @@ -39,17 +39,14 @@ Configuration is defined by `middleware.sql_to_arc.config.Config` and can be pro ```python config = Config.from_data({ - "db_name": "edaphobase", - "db_user": "postgres", - "db_password": "postgres", - "db_host": "localhost", - "rdi": "edaphobase", - "api_client": { - "api_url": "http://localhost:8000", - "client_cert_path": "/path/to/cert.pem", - "client_key_path": "/path/to/key.pem", - "verify_ssl": "false", - }, + "connection_string": "postgresql+asyncpg://user:pass@localhost:5432/edaphobase", + "rdi": "edaphobase", + "api_client": { + "api_url": "http://localhost:8000", + "client_cert_path": "/path/to/cert.pem", + "client_key_path": "/path/to/key.pem", + "verify_ssl": "false", + }, }) ``` diff --git a/middleware/sql_to_arc/config.example.yaml b/middleware/sql_to_arc/config.example.yaml index cc4735a..5830155 100644 --- a/middleware/sql_to_arc/config.example.yaml +++ b/middleware/sql_to_arc/config.example.yaml @@ -24,6 +24,7 @@ db_port: 5432 # This is used to tag or namespace the converted ARCs rdi: "edaphobase" + # API Client Configuration # ----------------------- # Settings for connecting to the Middleware API to upload ARCs diff --git a/middleware/sql_to_arc/pyproject.toml b/middleware/sql_to_arc/pyproject.toml index 54272b9..957e454 100644 --- a/middleware/sql_to_arc/pyproject.toml +++ b/middleware/sql_to_arc/pyproject.toml @@ -5,12 +5,16 @@ description = "The FAIRagro advanced middleware SQL-to-ARC converter" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "arctrl>=3.0.0b15", - "psycopg[binary]>=3.3.2", - "pydantic>=2.12.5", - "shared>=0.0.1", - "api_client>=0.0.1", - "opentelemetry-api>=1.39.1", + "arctrl>=3.0.0b15", + "pydantic>=2.12.5", + "shared>=0.0.1", + "api_client>=0.0.1", + "opentelemetry-api>=1.30.0", + "aiomysql>=0.2.0", + "aioodbc>=0.4.1", + "oracledb>=2.0.0", + "psycopg[binary]>=3.3.3", + "sqlalchemy[asyncio]>=2.0.46", ] [tool.hatch.version] diff --git a/middleware/sql_to_arc/src/middleware/sql_to_arc/builder.py b/middleware/sql_to_arc/src/middleware/sql_to_arc/builder.py new file mode 100644 index 0000000..de4c278 --- /dev/null +++ b/middleware/sql_to_arc/src/middleware/sql_to_arc/builder.py @@ -0,0 +1,340 @@ +"""ARC object building logic for the SQL-to-ARC conversion process.""" + +import gc +import json +import logging +from collections import defaultdict +from typing import Any, cast + +from arctrl import ( + ARC, + ArcAssay, + ArcStudy, + ArcTable, + CompositeCell, + CompositeHeader, + IOType, + OntologyAnnotation, +) + +from middleware.sql_to_arc.context import ArcBuildData +from middleware.sql_to_arc.mapper import ( + map_assay, + map_contact, + map_investigation, + map_publication, + map_study, +) +from middleware.sql_to_arc.models import ( + AssayRow, + ContactRow, + PublicationRow, + StudyRow, +) + +logger = logging.getLogger(__name__) + + +def _add_studies_to_arc(arc: ARC, study_rows: list[StudyRow]) -> dict[str, ArcStudy]: + """Add studies to ARC and return study map.""" + study_map: dict[str, ArcStudy] = {} + for s_row in study_rows: + study = map_study(s_row) + arc.AddRegisteredStudy(study) + study_map[str(s_row.identifier)] = study + return study_map + + +def _add_assays_to_arc(arc: ARC, assay_rows: list[AssayRow], study_map: dict[str, ArcStudy]) -> dict[str, ArcAssay]: + """Add assays to ARC, link to studies, and return assay map.""" + assay_map: dict[str, ArcAssay] = {} + for a_row in assay_rows: + assay = map_assay(a_row) + arc.AddAssay(assay) + assay_map[str(a_row.identifier)] = assay + + # Link Assay to Studies + _link_assay_to_studies(assay, a_row.study_ref, study_map) + + return assay_map + + +def _link_assay_to_studies(assay: ArcAssay, study_ref_val: Any, study_map: dict[str, ArcStudy]) -> None: + """Link an assay to one or more studies based on the study_ref value.""" + if not study_ref_val: + return + + if isinstance(study_ref_val, str): + try: + study_refs = json.loads(study_ref_val) + if isinstance(study_refs, list): + for s_ref in study_refs: + if str(s_ref) in study_map: + study_map[str(s_ref)].RegisterAssay(assay.Identifier) + return + except json.JSONDecodeError: + # Handle single ID if it's not JSON (fall through) + pass + + # Handle single ID (string or int) + if str(study_ref_val) in study_map: + study_map[str(study_ref_val)].RegisterAssay(assay.Identifier) + + +def _add_contacts_to_arc( + arc: ARC, + inv_id: str, + contacts: list[ContactRow], + study_map: dict[str, ArcStudy], + assay_map: dict[str, ArcAssay], +) -> None: + """Add contacts to investigation, studies, and assays.""" + # Investigation contacts + inv_contacts = [ + c for c in contacts if str(c.investigation_ref) == inv_id and getattr(c, "target_type", None) == "investigation" + ] + for c_row in inv_contacts: + arc.Contacts.append(map_contact(c_row)) + + # Study contacts + for s_id, study in study_map.items(): + stu_contacts = [ + c + for c in contacts + if str(c.investigation_ref) == inv_id + and getattr(c, "target_type", None) == "study" + and str(getattr(c, "target_ref", None)) == s_id + ] + for c_row in stu_contacts: + study.Contacts.append(map_contact(c_row)) + + # Assay contacts + for a_id, assay in assay_map.items(): + ass_contacts = [ + c + for c in contacts + if str(c.investigation_ref) == inv_id + and getattr(c, "target_type", None) == "assay" + and str(getattr(c, "target_ref", None)) == a_id + ] + for c_row in ass_contacts: + assay.Performers.append(map_contact(c_row)) + + +def _add_publications_to_arc( + arc: ARC, + inv_id: str, + publications: list[PublicationRow], + study_map: dict[str, ArcStudy], +) -> None: + """Add publications to investigation and studies.""" + # Investigation publications + inv_pubs = [ + p + for p in publications + if str(p.investigation_ref) == inv_id and getattr(p, "target_type", None) == "investigation" + ] + for p_row in inv_pubs: + arc.Publications.append(map_publication(p_row)) + + # Study publications + for s_id, study in study_map.items(): + stu_pubs = [ + p + for p in publications + if str(p.investigation_ref) == inv_id + and getattr(p, "target_type", None) == "study" + and str(getattr(p, "target_ref", None)) == s_id + ] + for p_row in stu_pubs: + study.Publications.append(map_publication(p_row)) + + +def _get_column_key(r: dict[str, Any]) -> tuple[Any, ...]: + """Extract a unique key for a column definition.""" + return ( + r.get("column_type"), + r.get("column_io_type"), + r.get("column_value"), + r.get("column_annotation_term"), + r.get("column_annotation_uri"), + r.get("column_annotation_version"), + r.get("column_name"), # Fallback for simple tests + ) + + +def _build_header(key: tuple[Any, ...]) -> CompositeHeader | None: + """Build a CompositeHeader from a column key tuple.""" + c_type, c_io, c_val, c_ann_term, c_ann_uri, c_ann_ver, c_name = key + try: + oa = OntologyAnnotation(c_ann_term or "", c_ann_uri or "", c_ann_ver or "") + + # Dispatch table for different header types + handlers = { + "input": lambda: CompositeHeader.input(IOType.of_string(c_io or "source_name")), + "output": lambda: CompositeHeader.output(IOType.of_string(c_io or "sample_name")), + "characteristic": lambda: CompositeHeader.characteristic(oa), + "factor": lambda: CompositeHeader.factor(oa), + "parameter": lambda: CompositeHeader.parameter(oa), + "component": lambda: CompositeHeader.component(oa), + "comment": lambda: CompositeHeader.comment(c_val or ""), + "performer": CompositeHeader.performer, + "date": CompositeHeader.date, + } + + if c_type in handlers: + return handlers[c_type]() + if c_name: + # Fallback for simple/untyped headers + return CompositeHeader.OfHeaderString(c_name) + + except (ValueError, TypeError, AttributeError) as e: + logger.warning("Failed to create header for type %s: %s", c_type, e) + return None + + +def _build_single_cell(cell_row: dict[str, Any], header: CompositeHeader) -> CompositeCell: + """Build a single CompositeCell from a database row.""" + cv = cell_row.get("cell_value") + cat = cell_row.get("cell_annotation_term") + cau = cell_row.get("cell_annotation_uri") or "" + cav = cell_row.get("cell_annotation_version") or "" + v = cell_row.get("value") # Fallback for old/simple tests + + # Unitized cell (value + ontology term) + if cv is not None and cat is not None: + return CompositeCell.unitized(str(cv), OntologyAnnotation(cat, cau, cav)) + + # Term cell (ontology term only) + if cat is not None: + return CompositeCell.term(OntologyAnnotation(cat, cau, cav)) + + # Text value? (either from new schema 'cell_value' or fallback 'value') + val_to_use = cv if cv is not None else v + if val_to_use is not None: + if header.IsTermColumn: + # If the column expects a term, wrap the text in an annotation + return CompositeCell.term(OntologyAnnotation(str(val_to_use), "", "")) + return CompositeCell.free_text(str(val_to_use)) + + return CompositeCell.free_text("") + + +def _build_column_cells( + rows_map: dict[int, dict[str, Any]], max_row_idx: int, header: CompositeHeader +) -> list[CompositeCell]: + """Build a list of CompositeCell objects for a column.""" + col_cells = [] + for idx in range(max_row_idx + 1): + cell_row = rows_map.get(idx) + if not cell_row: + col_cells.append(CompositeCell.free_text("")) + else: + col_cells.append(_build_single_cell(cell_row, header)) + return col_cells + + +def _build_arc_table(t_name: str, rows: list[dict[str, Any]]) -> ArcTable | None: + """Build an ArcTable from flat database rows.""" + if not rows: + return None + + table = ArcTable.init(t_name) + + # Determine max row index + max_row_idx = max((cast(int, r.get("row_index", 0)) for r in rows), default=-1) + if max_row_idx < 0: + return None + + col_keys: list[tuple[Any, ...]] = [] + seen_keys = set() + col_to_rows: dict[tuple[Any, ...], dict[int, dict[str, Any]]] = defaultdict(dict) + + for r in rows: + key = _get_column_key(r) + if key not in seen_keys: + col_keys.append(key) + seen_keys.add(key) + col_to_rows[key][cast(int, r.get("row_index", 0))] = r + + for key in col_keys: + header = _build_header(key) + if not header: + continue + + # Build Cells for this column + col_cells = _build_column_cells(col_to_rows[key], max_row_idx, header) + table.AddColumn(header, col_cells) + + return table + + +def _process_annotation_tables( + inv_id: str, annotations: list[dict[str, Any]], study_map: dict[str, Any], assay_map: dict[str, Any] +) -> None: + """Process and add annotation tables.""" + tables_groups = defaultdict(list) + for ann in annotations: + if ann.get("investigation_ref") == inv_id: + key = (ann.get("target_type"), ann.get("target_ref"), ann.get("table_name")) + tables_groups[key].append(ann) + + for (t_type, t_ref, t_name), rows in tables_groups.items(): + if not t_name: + continue + + target = None + if t_type == "study" and isinstance(t_ref, str): + target = study_map.get(t_ref) + elif t_type == "assay" and isinstance(t_ref, str): + target = assay_map.get(t_ref) + + if target: + table = _build_arc_table(t_name, rows) + if table: + target.AddTable(table) + + +def build_single_arc_task(data: ArcBuildData) -> str: + """Build a single ARC object from data. + + This function is designed to run in a separate process. + It returns the JSON representation to minimize memory footprint in the main process. + """ + inv_id = str(data.investigation_row.identifier) + + try: + # Map Investigation and create ARC + arc_inv = map_investigation(data.investigation_row) + arc = ARC.from_arc_investigation(arc_inv) + + # Identify relevant studies and assays + relevant_studies = [s for s in data.studies if str(s.investigation_ref) == inv_id] + relevant_assays = [a for a in data.assays if str(a.investigation_ref) == inv_id] + + # Add studies and assays + study_map = _add_studies_to_arc(arc, relevant_studies) + assay_map = _add_assays_to_arc(arc, relevant_assays, study_map) + + # Add contacts and publications + _add_contacts_to_arc(arc, inv_id, data.contacts, study_map, assay_map) + _add_publications_to_arc(arc, inv_id, data.publications, study_map) + + # Process annotation tables + _process_annotation_tables(inv_id, data.annotations, study_map, assay_map) + + # Serialize immediately in the worker process + json_str: str = arc.ToROCrateJsonString() + + # Explicitly clean up memory before returning + del arc + del arc_inv + del study_map + del assay_map + gc.collect() + + return json_str + + except Exception: + gc.collect() + raise diff --git a/middleware/sql_to_arc/src/middleware/sql_to_arc/config.py b/middleware/sql_to_arc/src/middleware/sql_to_arc/config.py index d3071ed..5c3cf28 100644 --- a/middleware/sql_to_arc/src/middleware/sql_to_arc/config.py +++ b/middleware/sql_to_arc/src/middleware/sql_to_arc/config.py @@ -3,7 +3,6 @@ from typing import Annotated, Any from pydantic import Field, SecretStr, model_validator -from pydantic_core import PydanticUndefined from middleware.api_client.config import Config as ApiClientConfig from middleware.shared.config.config_base import ConfigBase @@ -12,11 +11,11 @@ class Config(ConfigBase): """Configuration model for the Middleware API.""" - db_name: Annotated[str, Field(description="Database name")] - db_user: Annotated[str, Field(description="Database user")] - db_password: Annotated[SecretStr, Field(description="Database password")] - db_host: Annotated[str, Field(description="Database host")] - db_port: Annotated[int, Field(description="Database port")] = 5432 + connection_string: Annotated[SecretStr, Field(description="Database connection string")] + debug_limit: Annotated[ + int | None, + Field(description="Debug limit for investigations (optional)", gt=0), + ] = None rdi: Annotated[str, Field(description="RDI identifier (e.g. edaphobase)")] rdi_url: Annotated[str, Field(description="URL of the Source RDI (for provenance in report)")] max_concurrent_arc_builds: Annotated[ @@ -29,13 +28,12 @@ class Config(ConfigBase): max_concurrent_tasks: Annotated[ int, Field( - default=PydanticUndefined, # Satisfy mypy, validator will set the 4x default description=( "Maximum number of parallel tasks (IO + CPU). Defaults to 4x max_concurrent_arc_builds if not provided." ), ge=1, ), - ] + ] = None # type: ignore db_batch_size: Annotated[ int, Field( @@ -43,7 +41,6 @@ class Config(ConfigBase): ge=1, ), ] = 100 - api_client: Annotated[ApiClientConfig, Field(description="API Client configuration")] max_studies: Annotated[ int, Field( @@ -65,14 +62,15 @@ class Config(ConfigBase): ge=1, ), ] = 30 + api_client: Annotated[ApiClientConfig, Field(description="API Client configuration")] @model_validator(mode="before") @classmethod def set_default_max_concurrent_tasks(cls, data: Any) -> Any: """Set default max_concurrent_tasks if not provided.""" - if isinstance(data, dict) and "max_concurrent_tasks" not in data: + if isinstance(data, dict) and ("max_concurrent_tasks" not in data or data["max_concurrent_tasks"] is None): field_info = cls.model_fields.get("max_concurrent_arc_builds") - default_max_builds = getattr(field_info, "default", 5) # A default for the default value. + default_max_builds = getattr(field_info, "default", 5) max_builds = data.get("max_concurrent_arc_builds", default_max_builds) data["max_concurrent_tasks"] = int(max_builds) * 4 return data diff --git a/middleware/sql_to_arc/src/middleware/sql_to_arc/context.py b/middleware/sql_to_arc/src/middleware/sql_to_arc/context.py new file mode 100644 index 0000000..3f52b48 --- /dev/null +++ b/middleware/sql_to_arc/src/middleware/sql_to_arc/context.py @@ -0,0 +1,56 @@ +"""Internal context models for the SQL-to-ARC processing workflow.""" + +import concurrent.futures +from dataclasses import dataclass +from typing import Any + +from middleware.api_client import ApiClient +from middleware.sql_to_arc.models import ( + AssayRow, + ContactRow, + InvestigationRow, + PublicationRow, + StudyRow, +) + + +@dataclass(frozen=True, slots=True) +class ArcBuildData: + """Data bundle for building a single ARC.""" + + investigation_row: InvestigationRow + studies: list[StudyRow] + assays: list[AssayRow] + contacts: list[ContactRow] + publications: list[PublicationRow] + annotations: list[dict[str, Any]] + + +@dataclass(frozen=True, slots=True) +class WorkerContext: + """Context data for a worker process, combining API client and pre-fetched data.""" + + client: ApiClient + rdi: str + studies_by_inv: dict[str, list[StudyRow]] + assays_by_inv: dict[str, list[AssayRow]] + contacts_by_inv: dict[str, list[ContactRow]] + pubs_by_inv: dict[str, list[PublicationRow]] + anns_by_inv: dict[str, list[dict[str, Any]]] + worker_id: int + total_workers: int + executor: concurrent.futures.Executor + arc_generation_timeout_minutes: int = 30 + + +@dataclass(frozen=True, slots=True) +class RelatedDataBatch: + """Batch of related data grouped by investigation ID.""" + + studies_by_inv: dict[str, list[StudyRow]] + assays_by_inv: dict[str, list[AssayRow]] + contacts_by_inv: dict[str, list[ContactRow]] + pubs_by_inv: dict[str, list[PublicationRow]] + anns_by_inv: dict[str, list[dict[str, Any]]] + study_count: int + assay_count: int diff --git a/middleware/sql_to_arc/src/middleware/sql_to_arc/database.py b/middleware/sql_to_arc/src/middleware/sql_to_arc/database.py new file mode 100644 index 0000000..e069118 --- /dev/null +++ b/middleware/sql_to_arc/src/middleware/sql_to_arc/database.py @@ -0,0 +1,312 @@ +"""Database module for SQL-to-ARC.""" + +import logging +from collections.abc import AsyncGenerator, Iterable +from contextlib import asynccontextmanager +from typing import Any, TypeVar, cast + +import sqlalchemy +from pydantic import ValidationError +from sqlalchemy import ( + column, + func, + inspect, + select, + table, +) +from sqlalchemy.exc import NoSuchTableError, ProgrammingError +from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine + +from middleware.sql_to_arc.models import ( + AssayRow, + BaseRow, + ContactRow, + InvestigationRow, + MissingRequiredColumnsError, + PublicationRow, + RequiredColumnsNullError, + StudyRow, +) +from middleware.sql_to_arc.stats import ProcessingStats + +logger = logging.getLogger(__name__) +RowModel = TypeVar("RowModel", bound=BaseRow) + + +class SchemaValidator: + """Validator for database schema and structural integrity.""" + + def __init__(self, engine: AsyncEngine) -> None: + """Initialize with database engine.""" + self.engine = engine + + async def validate_models(self, models: Iterable[type[BaseRow]]) -> None: + """Validate all provided models against the database schema.""" + async with self.engine.connect() as conn: + for model in models: + await self._validate_model(conn, model) + + async def _validate_model(self, conn: AsyncConnection, model: type[BaseRow]) -> None: + """Validate a single model against its corresponding database view.""" + view_name = getattr(model, "__view_name__", None) + if not view_name: + logger.debug("Skipping validation for model %s (no __view_name__)", model.__name__) + return + + db_columns = await self._get_db_columns(conn, view_name) + if db_columns is None: + return + + self._check_column_presence(model, db_columns) + await self._check_null_values(conn, model, db_columns) + + @staticmethod + async def _get_db_columns(conn: AsyncConnection, view_name: str) -> set[str] | None: + """Retrieve column names for a given table or view.""" + try: + columns = await conn.run_sync(lambda sync_conn: inspect(sync_conn).get_columns(view_name)) + return {col["name"] for col in columns} + except (ProgrammingError, NoSuchTableError): + logger.warning('Table or view "%s" does not exist or is not accessible.', view_name) + return None + + @staticmethod + def _check_column_presence(model: type[BaseRow], db_columns: set[str]) -> None: + """Check for missing required/optional columns and extra columns.""" + model_fields = model.model_fields + present_fields = set(model_fields.keys()) + missing_required: list[str] = [] + missing_optional: list[str] = [] + + for field_name, field_info in model_fields.items(): + if field_name in db_columns: + continue + + json_extra = field_info.json_schema_extra + spec_required = json_extra.get("spec_required") if isinstance(json_extra, dict) else None + is_required = field_info.is_required() if spec_required is None else spec_required + + if is_required and field_info.is_required(): + missing_required.append(field_name) + else: + missing_optional.append(field_name) + + if missing_required: + raise MissingRequiredColumnsError(model.__name__, sorted(missing_required)) + + if missing_optional: + logger.warning( + 'Table "%s" is missing optional columns: %s. Using default values.', + model.__name__, + ", ".join(sorted(missing_optional)), + ) + + extra_columns = db_columns - present_fields + if extra_columns: + logger.info( + 'Table "%s" contains extra columns not used by model: %s.', + model.__name__, + ", ".join(sorted(extra_columns)), + ) + + @staticmethod + async def _check_null_values(conn: AsyncConnection, model: type[BaseRow], db_columns: set[str]) -> None: + """Check for NULL values in required fields.""" + view_name = model.__view_name__ + for field_name, field_info in model.model_fields.items(): + if field_name not in db_columns: + continue + + json_extra = field_info.json_schema_extra + spec_required = json_extra.get("spec_required") if isinstance(json_extra, dict) else None + if spec_required is False: + continue + + # If not explicitly marked as NOT spec_required, and has no default, it's mandatory + if spec_required or field_info.is_required(): + allow_override = json_extra.get("spec_override", False) if isinstance(json_extra, dict) else False + + # Use SQLAlchemy select() to count NULLs + t = table(view_name, column(field_name)) + stmt = select(func.count()).select_from(t).where(column(field_name).is_(None)) # pylint: disable=not-callable + result = await conn.execute(stmt) + null_count = result.scalar() or 0 + + if null_count > 0: + if allow_override: + logger.warning( + 'Table "%s": Column "%s" contains %d NULL values. ' + "These will be replaced by model defaults due to allow_spec_override=True.", + model.__name__, + field_name, + null_count, + ) + else: + raise RequiredColumnsNullError(model.__name__, [field_name]) + + +class Database: + """Database handler using SQLAlchemy.""" + + def __init__(self, connection_string: str) -> None: + """Initialize database with connection string.""" + # Use modern async drivers for SQLAlchemy connections + if connection_string.startswith("postgresql://"): + connection_string = connection_string.replace("postgresql://", "postgresql+psycopg://", 1) + elif connection_string.startswith("mysql://") or connection_string.startswith("mariadb://"): + connection_string = connection_string.replace("mysql://", "mysql+aiomysql://", 1).replace( + "mariadb://", "mysql+aiomysql://", 1 + ) + elif connection_string.startswith("oracle://"): + connection_string = connection_string.replace("oracle://", "oracle+oracledb://", 1) + elif connection_string.startswith("mssql://"): + connection_string = connection_string.replace("mssql://", "mssql+aioodbc://", 1) + + self.engine: AsyncEngine = create_async_engine(connection_string, echo=False) + self.validator = SchemaValidator(self.engine) + + async def validate_schema(self) -> None: + """Validate schema for all known models.""" + models = [ + InvestigationRow, + StudyRow, + AssayRow, + ContactRow, + PublicationRow, + ] + # Cast to satisfying the Iterable[type[BaseRow]] requirement + await self.validator.validate_models(cast(Iterable[type[BaseRow]], models)) + + @staticmethod + def _validate_and_map( + row: Any, + model: type[RowModel], + entity_name: str, + ) -> RowModel | None: + try: + validated: RowModel = model.model_validate(dict(row)) + return validated + except ValidationError as error: + logger.warning("Skipping %s due to validation error: %s", entity_name, error) + return None + + async def stream_investigations( + self, + stats: ProcessingStats, + limit: int | None = None, + ) -> AsyncGenerator[InvestigationRow, None]: + """Stream investigations using a server-side cursor.""" + view_name = InvestigationRow.__view_name__ + try: + async with self.engine.connect() as conn: + # Use literal_column("*") to ensure SQLAlchemy generates 'SELECT *' + # instead of '"vInvestigation"."*"' + stmt: sqlalchemy.Select[Any] = ( + select(sqlalchemy.literal_column("*")) + .select_from(table(view_name)) + .execution_options(stream_results=True) + ) + if limit: + stmt = stmt.limit(limit) + + result = await conn.stream(stmt) + async for row in result.mappings(): + # Count everything we find in the database + stats.found_datasets += 1 + + investigation = self._validate_and_map(row, InvestigationRow, "investigation") + if investigation is None: + # If validation fails, it's a found but failed dataset + stats.failed_datasets += 1 + stats.failed_ids.append(row.get("identifier", "unknown")) + continue + + yield investigation + except ProgrammingError as e: + if f'relation "{view_name.lower()}" does not exist' in str(e).lower(): + logger.warning('Table or view "%s" does not exist. Treating as empty.', view_name) + else: + raise + + async def _stream_by_investigation( + self, + model: type[RowModel], + investigation_ids: list[str], + entity_name: str, + ) -> AsyncGenerator[RowModel, None]: + """Stream related data for a given set of investigation IDs.""" + if not investigation_ids: + return + view_name = model.__view_name__ + try: + async with self.engine.connect() as conn: + # Use literal_column("*") to select all columns + c_inv_ref: sqlalchemy.ColumnElement[Any] = column("investigation_ref") + stmt: sqlalchemy.Select[Any] = ( + select(sqlalchemy.literal_column("*")) + .select_from(table(view_name)) + .where(c_inv_ref.in_(investigation_ids)) + .execution_options(stream_results=True) + ) + + result = await conn.stream(stmt) + async for row in result.mappings(): + item = self._validate_and_map(row, model, entity_name) + if item is not None: + yield item + except ProgrammingError as e: + if f'relation "{view_name.lower()}" does not exist' in str(e).lower(): + logger.warning('Table or view "%s" does not exist. Treating as empty.', view_name) + else: + raise + + async def stream_studies(self, investigation_ids: list[str]) -> AsyncGenerator[StudyRow, None]: + """Stream studies for given investigations.""" + async for r in self._stream_by_investigation(StudyRow, investigation_ids, "study"): + yield r + + async def stream_assays(self, investigation_ids: list[str]) -> AsyncGenerator[AssayRow, None]: + """Stream assets for given investigations.""" + async for r in self._stream_by_investigation(AssayRow, investigation_ids, "assay"): + yield r + + async def stream_contacts(self, investigation_ids: list[str]) -> AsyncGenerator[ContactRow, None]: + """Stream contacts for given investigations.""" + async for r in self._stream_by_investigation(ContactRow, investigation_ids, "contact"): + yield r + + async def stream_publications(self, investigation_ids: list[str]) -> AsyncGenerator[PublicationRow, None]: + """Stream publications for given investigations.""" + async for r in self._stream_by_investigation(PublicationRow, investigation_ids, "publication"): + yield r + + async def stream_annotation_tables(self, investigation_ids: list[str]) -> AsyncGenerator[dict[str, Any], None]: + """Stream annotation tables for given investigations.""" + if not investigation_ids: + return + view_name = "vAnnotationTable" + try: + async with self.engine.connect() as conn: + # Use literal_column("*") to select all columns + c_inv_ref: sqlalchemy.ColumnElement[Any] = column("investigation_ref") + stmt: sqlalchemy.Select[Any] = ( + select(sqlalchemy.literal_column("*")) + .select_from(table(view_name)) + .where(c_inv_ref.in_(investigation_ids)) + .execution_options(stream_results=True) + ) + + result = await conn.stream(stmt) + async for row in result.mappings(): + yield dict(row) + except ProgrammingError as e: + if f'relation "{view_name.lower()}" does not exist' in str(e).lower(): + logger.warning('Table or view "%s" does not exist. Treating as empty.', view_name) + else: + raise + + @asynccontextmanager + async def connect(self) -> AsyncGenerator[AsyncConnection, None]: + """Context manager for database connection.""" + async with self.engine.connect() as conn: + yield conn diff --git a/middleware/sql_to_arc/src/middleware/sql_to_arc/main.py b/middleware/sql_to_arc/src/middleware/sql_to_arc/main.py index 84c5827..5719e0d 100644 --- a/middleware/sql_to_arc/src/middleware/sql_to_arc/main.py +++ b/middleware/sql_to_arc/src/middleware/sql_to_arc/main.py @@ -1,119 +1,31 @@ -"""SQL-to-ARC middleware component.""" +"""SQL-to-ARC middleware component entry point.""" import argparse import asyncio -import concurrent.futures -import gc -import json import logging import multiprocessing -import sys import time -from collections import defaultdict -from collections.abc import AsyncGenerator from importlib.metadata import PackageNotFoundError, version from pathlib import Path -from typing import Any -import psycopg -from arctrl import ARC, ArcInvestigation # type: ignore[import-untyped] -from opentelemetry import trace -from psycopg.rows import dict_row -from pydantic import BaseModel, ConfigDict, ValidationError +from pydantic import ValidationError -from middleware.api_client import ApiClient, ApiClientError +from middleware.api_client import ApiClient from middleware.shared.config.config_wrapper import ConfigWrapper from middleware.shared.config.logging import configure_logging from middleware.shared.tracing import initialize_tracing from middleware.sql_to_arc.config import Config -from middleware.sql_to_arc.mapper import ( - map_assay, - map_investigation, - map_study, -) +from middleware.sql_to_arc.database import Database +from middleware.sql_to_arc.processor import process_investigations +from middleware.sql_to_arc.stats import ProcessingStats # Configure logging logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s") -# Suppress noisy library logs at INFO level -logging.getLogger("httpx").setLevel(logging.WARNING) -logging.getLogger("httpcore").setLevel(logging.WARNING) logger = logging.getLogger(__name__) -class ProcessingStats(BaseModel): - """Statistics for the conversion process.""" - - found_datasets: int = 0 - total_studies: int = 0 - total_assays: int = 0 - failed_datasets: int = 0 - failed_ids: list[str] = [] - duration_seconds: float = 0.0 - - model_config = ConfigDict(arbitrary_types_allowed=True) - - def merge(self, other: "ProcessingStats") -> None: - """Merge another stats object into this one.""" - self.found_datasets += other.found_datasets - self.failed_datasets += other.failed_datasets - self.failed_ids.extend(other.failed_ids) - # Note: total_studies, total_assays are counted centrally, not merged from workers - - def to_jsonld(self, rdi_identifier: str | None = None, rdi_url: str | None = None) -> str: - """Return JSON-LD representation of stats using Schema.org and PROV terms.""" - # Convert duration to ISO 8601 duration format (PTx.xS) - duration_iso = f"PT{self.duration_seconds:.2f}S" - - ld_struct = { - "@context": { - "schema": "http://schema.org/", - "prov": "http://www.w3.org/ns/prov#", - "void": "http://rdfs.org/ns/void#", - "xsd": "http://www.w3.org/2001/XMLSchema#", - # Map duration to schema:duration (Expects ISO 8601 string) - "duration": {"@id": "schema:duration", "@type": "schema:Duration"}, - # Map failed IDs to schema:error (list of strings) - "failed_ids": {"@id": "schema:error", "@container": "@set"}, - # Map status - "status": {"@id": "schema:actionStatus"}, - # Use VoID for counts (statistic items) - "found_datasets": {"@id": "void:entities", "@type": "xsd:integer"}, - # Custom descriptive terms for study/assay counts as they are domain specific - # We map them to schema:result for semantics, but keep key names - "total_studies": {"@id": "schema:result", "@type": "xsd:integer"}, - "total_assays": {"@id": "schema:result", "@type": "xsd:integer"}, - }, - "@type": ["prov:Activity", "schema:CreateAction"], - "schema:name": "SQL to ARC Conversion Run", - "schema:instrument": { - "@type": "schema:SoftwareApplication", - "schema:name": "FAIRagro Middleware SQL-to-ARC", - }, - # Process status - "status": ("schema:CompletedActionStatus" if self.failed_datasets == 0 else "schema:FailedActionStatus"), - # Metrics - "duration": duration_iso, - "duration_seconds": round(self.duration_seconds, 2), # Keep raw float for easy parsing - "found_datasets": self.found_datasets, - "total_studies": self.total_studies, - "total_assays": self.total_assays, - "failed_datasets": self.failed_datasets, - "failed_ids": sorted(self.failed_ids), - } - - if rdi_identifier and rdi_url: - ld_struct["prov:used"] = { - "@id": rdi_url, - "@type": "schema:Organization", # RDI acts as an Organization/Service - "schema:identifier": rdi_identifier, - "schema:name": f"Research Data Infrastructure: {rdi_identifier}", - } - - return json.dumps(ld_struct, indent=2) - - -def parse_args() -> argparse.Namespace: - """Parse command line arguments, ignoring unknown args (e.g., pytest flags).""" +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + """Parse command line arguments.""" parser = argparse.ArgumentParser(description="SQL to ARC Converter") parser.add_argument( "-c", @@ -128,392 +40,33 @@ def parse_args() -> argparse.Namespace: action="store_true", help="Show version and exit", ) - args, _ = parser.parse_known_args() + args = parser.parse_args(argv) return args -def build_single_arc_task( - investigation_row: dict[str, Any], - studies: list[dict[str, Any]], - assays_by_study: dict[int, list[dict[str, Any]]], -) -> ArcInvestigation: - """Build a single ARC investigation object. - - This function is designed to run in a separate process. - """ - arc = map_investigation(investigation_row) - - for study_row in studies: - study = map_study(study_row) - arc.AddRegisteredStudy(study) - - # Add assays for this study - assays_rows = assays_by_study.get(study_row["id"], []) - for assay_row in assays_rows: - assay = map_assay(assay_row) - study.AddRegisteredAssay(assay) - - return arc - - -async def stream_investigation_datasets( - cur: psycopg.AsyncCursor[dict[str, Any]], batch_size: int = 100 -) -> "AsyncGenerator[tuple[dict[str, Any], list[dict[str, Any]], dict[int, list[dict[str, Any]]]], None]": - """Stream investigation datasets (inv + studies + assays) in batches. - - This avoids loading the entire database into memory. - - Args: - cur: Database cursor. - batch_size: Number of investigations to fetch and process details for at once. - - Yields: - Tuple of (investigation_row, studies_list, assays_by_study_dict). - """ - # Use a server-side cursor if it has a name, otherwise it's client-side. - # To be safe and compatible, we'll just execute and chunk the results if needed, - # or rely on the cursor being a server-side one from the caller. - await cur.execute('SELECT id, title, description, submission_time, release_time FROM "ARC_Investigation"') - - while True: - rows = await cur.fetchmany(batch_size) - if not rows: - break - - investigation_ids = [row["id"] for row in rows] - - if not cur.connection: - raise RuntimeError("Cursor has no connection attached") - - # Fetch studies for this batch using a separate cursor - # We MUST use a separate cursor because executing on 'cur' would Close - # the current result set (investigations) if it's not fully consumed/server-side. - # Even with server-side cursors, it's safer to use a dedicated cursor for nested queries. - async with cur.connection.cursor(row_factory=dict_row) as detail_cur: - await detail_cur.execute( - "SELECT id, investigation_id, title, description, submission_time, release_time " - 'FROM "ARC_Study" WHERE investigation_id = ANY(%s)', - (investigation_ids,), - ) - study_rows = await detail_cur.fetchall() - studies_by_inv: dict[int, list[dict[str, Any]]] = defaultdict(list) - for s in study_rows: - studies_by_inv[s["investigation_id"]].append(s) - - # Fetch assays for these studies - study_ids = [s["id"] for s in study_rows] - assays_by_study: dict[int, list[dict[str, Any]]] = defaultdict(list) - if study_ids: - await detail_cur.execute( - 'SELECT id, study_id, measurement_type, technology_type FROM "ARC_Assay" WHERE study_id = ANY(%s)', - (study_ids,), - ) - assay_rows = await detail_cur.fetchall() - for a in assay_rows: - assays_by_study[a["study_id"]].append(a) - - for inv_row in rows: - inv_id = inv_row["id"] - yield inv_row, studies_by_inv[inv_id], assays_by_study - - -# Removed fetch_studies_bulk and fetch_assays_bulk as they are now integrated into stream_investigation_datasets - - -def build_arc_for_investigation( - investigation_row: dict[str, Any], - studies: list[dict[str, Any]], - assays_by_study: dict[int, list[dict[str, Any]]], -) -> str: - """Build a single ARC for an investigation (CPU-bound operation for ProcessPoolExecutor). - - This function is designed to be called in a separate process and returns - the JSON representation to minimize memory footprint in the main process. - - Args: - investigation_row: Investigation database row. - studies: List of studies for this investigation. - assays_by_study: Dictionary mapping study_id to list of assays. - - Returns: - JSON string representation of the ARC. - """ - try: - # Filter assays for these studies - relevant_assays = {s["id"]: assays_by_study.get(s["id"], []) for s in studies} - - # Build ArcInvestigation - arc_investigation = build_single_arc_task(investigation_row, studies, relevant_assays) - - # Wrap in ARC container - arc = ARC.from_arc_investigation(arc_investigation) - - # Serialize immediately in the worker process - json_str: str = arc.ToROCrateJsonString() - - # Explicitly clean up memory before returning - del arc - del arc_investigation - gc.collect() - - return json_str - except Exception: - gc.collect() - raise - - -class WorkerContext(BaseModel): - """Context data for a worker process.""" - - client: Any # ApiClient, but Any to allow mocking - rdi: str - executor: Any # ProcessPoolExecutor is not Pydantic-friendly easily, so Any - max_studies: int - max_assays: int - arc_generation_timeout_minutes: int - - model_config = ConfigDict(arbitrary_types_allowed=True) - - -class DatasetContext(BaseModel): - """Context for a single investigation dataset (investigation, studies, assays).""" - - investigation_row: dict[str, Any] - studies: list[dict[str, Any]] - assays_by_study: dict[int, list[dict[str, Any]]] - - model_config = ConfigDict(arbitrary_types_allowed=True) - - -async def process_single_dataset( - ctx: WorkerContext, - dataset_ctx: DatasetContext, - semaphore: asyncio.Semaphore, - stats: ProcessingStats, -) -> None: - """Process a single investigation: Build -> Serialize -> Log -> Upload. - - Args: - ctx: Worker context (client, executor, etc). - dataset_ctx: DatasetContext containing investigation, studies, and assays. - semaphore: Semaphore to limit concurrent active tasks. - stats: Stats object to update (mutable). - """ - log_prefix = f"[InvID: {dataset_ctx.investigation_row['id']}]" - - # Acquire semaphore to limit concurrency - async with semaphore: - try: - # 1. Prepare data (already gathered by stream) - # Count details for stats/logging - num_studies = len(dataset_ctx.studies) - num_assays = sum(len(dataset_ctx.assays_by_study.get(s["id"], [])) for s in dataset_ctx.studies) - stats.total_studies += num_studies - stats.total_assays += num_assays - - logger.info( - "%s Starting ARC build. Content: %d studies, %d assays.", - log_prefix, - num_studies, - num_assays, - ) - - # Check size limits - if num_studies > ctx.max_studies: - logger.warning( - "%s Skipping: study count (%d) exceeds limit (%d).", - log_prefix, - num_studies, - ctx.max_studies, - ) - stats.failed_datasets += 1 - stats.failed_ids.append(str(dataset_ctx.investigation_row["id"])) - return - - if num_assays > ctx.max_assays: - logger.warning( - "%s Skipping: assay count (%d) exceeds limit (%d).", - log_prefix, - num_assays, - ctx.max_assays, - ) - stats.failed_datasets += 1 - stats.failed_ids.append(str(dataset_ctx.investigation_row["id"])) - return - - # 2. Build & Serialize ARC (CPU-bound) -> Offload to ProcessPool - # We return the JSON string directly from the worker to allow early GC of ARC objects - try: - json_str = await asyncio.wait_for( - asyncio.get_event_loop().run_in_executor( - ctx.executor, - build_arc_for_investigation, - dataset_ctx.investigation_row, - dataset_ctx.studies, - dataset_ctx.assays_by_study, - ), - timeout=ctx.arc_generation_timeout_minutes * 60, - ) - except TimeoutError: - logger.error( - "%s ARC generation timed out after %d minutes.", - log_prefix, - ctx.arc_generation_timeout_minutes, - ) - stats.failed_datasets += 1 - stats.failed_ids.append(str(dataset_ctx.investigation_row["id"])) - return - - if not json_str: - logger.error("%s ARC build/serialization failed", log_prefix) - stats.failed_datasets += 1 - stats.failed_ids.append(str(dataset_ctx.investigation_row["id"])) - return - - logger.info( - "%s ARC build & serialization complete. Payload size: %.2f MB. Uploading...", - log_prefix, - len(json_str.encode("utf-8")) / (1024 * 1024), - ) - - # 4. Upload (IO-bound) - response = await ctx.client.create_or_update_arc( - rdi=ctx.rdi, - arc=json.loads(json_str), - ) - # Use status from response if available (e.g., 'created', 'updated') - status_text = "processed" - if response.arc: - status_text = response.arc.status.value - - logger.info( - "%s ARC %s successfully (RDI: %s).", - log_prefix, - status_text, - ctx.rdi, - ) - - except (ApiClientError, psycopg.Error, OSError) as e: - logger.error("%s Processing failed: %s", log_prefix, e) - stats.failed_datasets += 1 - stats.failed_ids.append(str(dataset_ctx.investigation_row["id"])) - except Exception as e: # pylint: disable=broad-exception-caught - logger.error("%s Unexpected error: %s", log_prefix, e, exc_info=True) - stats.failed_datasets += 1 - stats.failed_ids.append(str(dataset_ctx.investigation_row["id"])) - - -async def process_investigations( - cur: psycopg.AsyncCursor[dict[str, Any]], - client: ApiClient, - config: Config, -) -> ProcessingStats: - """Fetch investigations from DB and process them concurrently. - - Args: - cur: Database cursor. - client: API client instance. - config: Configuration object. - - Returns: - ProcessingStats. - """ - stats = ProcessingStats() - with trace.get_tracer(__name__).start_as_current_span("sql_to_arc.main.process_investigations"): - # Step 1: Initialize concurrency control - semaphore = asyncio.Semaphore(config.max_concurrent_tasks) - logger.info( - "Starting streaming processing: CPU_workers=%d, Max_tasks=%d", - config.max_concurrent_arc_builds, - config.max_concurrent_tasks, - ) - - # Use ProcessPoolExecutor for CPU offloading - with concurrent.futures.ProcessPoolExecutor( - max_workers=config.max_concurrent_arc_builds, mp_context=multiprocessing.get_context("spawn") - ) as executor: - ctx = WorkerContext( - client=client, - rdi=config.rdi, - executor=executor, - max_studies=config.max_studies, - max_assays=config.max_assays, - arc_generation_timeout_minutes=config.arc_generation_timeout_minutes, - ) - - # Step 2: Stream and spawn tasks - # We use a set of tasks to keep track of running operations - running_tasks: set[asyncio.Task] = set() - - async for item in stream_investigation_datasets(cur, batch_size=config.db_batch_size): - stats.found_datasets += 1 - - # Backlog Flow Control: Prevent reading too much from DB if workers are busy. - # If we have reached the max number of concurrent tasks, wait for one to finish. - # This keeps the memory footprint under control by stopping the stream producer. - if len(running_tasks) >= config.max_concurrent_tasks: - await asyncio.wait(running_tasks, return_when=asyncio.FIRST_COMPLETED) - - dataset_ctx = DatasetContext( - investigation_row=item[0], - studies=item[1], - assays_by_study=item[2], - ) - - # Create the processing task - # Note: process_single_dataset itself handles the semaphore - task = asyncio.create_task(process_single_dataset(ctx, dataset_ctx, semaphore, stats)) - running_tasks.add(task) - - # Cleanup finished tasks periodically to keep memory low - task.add_done_callback(running_tasks.discard) - - # Wait for all remaining tasks to finish - if running_tasks: - logger.info("Waiting for %d remaining tasks to complete...", len(running_tasks)) - await asyncio.gather(*running_tasks) - - return stats - - async def run_conversion(config: Config) -> ProcessingStats: - """Run the SQL-to-ARC conversion with the given configuration. + """Run the conversion.""" + db = Database(config.connection_string.get_secret_value()) - Args: - config: Configuration object. + # 1. Validate DB schema before starting + await db.validate_schema() - Returns: - ProcessingStats. - """ - tracer = trace.get_tracer(__name__) - with tracer.start_as_current_span("sql_to_arc.main.run_conversion"): - async with ( - ApiClient(config.api_client) as client, - await psycopg.AsyncConnection.connect( - dbname=config.db_name, - user=config.db_user, - password=config.db_password.get_secret_value(), - host=config.db_host, - port=config.db_port, - ) as conn, - conn.cursor(row_factory=dict_row) as cur, - ): - return await process_investigations(cur, client, config) + async with ApiClient(config.api_client) as client: + return await process_investigations(db, client, config) -async def main() -> None: - """Connect to DB, process investigations, and upload ARCs.""" - args = parse_args() +async def main(argv: list[str] | None = None) -> None: + """Execute the main entry point.""" + args = parse_args(argv) if args.version: try: print(f"sql_to_arc version: {version('sql_to_arc')}") except PackageNotFoundError: print("sql_to_arc version: unknown (package not installed)") - sys.exit(0) + return try: - # Load config via ConfigWrapper so ENV/Secrets with prefix 'SQL_TO_ARC' are respected wrapper = ConfigWrapper.from_yaml_file(args.config, prefix="SQL_TO_ARC") config = Config.from_config_wrapper(wrapper) configure_logging(config.log_level) @@ -521,7 +74,6 @@ async def main() -> None: logger.error("Failed to load configuration: %s", e) return - # Initialize OpenTelemetry tracing otlp_endpoint = str(config.otel.endpoint) if config.otel.endpoint else None _tracer_provider, tracer = initialize_tracing( service_name="sql_to_arc", @@ -529,9 +81,8 @@ async def main() -> None: log_console_spans=config.otel.log_console_spans, ) - with tracer.start_as_current_span("sql_to_arc.main.main"): + with tracer.start_as_current_span("sql_to_arc.main"): logger.info("Starting SQL-to-ARC conversion with config: %s", args.config) - try: start_time = time.perf_counter() stats = await run_conversion(config) @@ -539,11 +90,8 @@ async def main() -> None: stats.duration_seconds = end_time - start_time logger.info("SQL-to-ARC conversion completed. Report:") - print( - stats.to_jsonld(rdi_identifier=config.rdi, rdi_url=config.rdi_url) - ) # Print to stdout as requested for report + print(stats.to_jsonld(rdi_identifier=config.rdi, rdi_url=config.rdi_url)) - # Log final summary if stats.failed_datasets > 0: logger.warning( "Conversion finished with %d failures out of %d datasets.", @@ -551,10 +99,7 @@ async def main() -> None: stats.found_datasets, ) else: - logger.info( - "Conversion finished successfully. %d datasets processed.", - stats.found_datasets, - ) + logger.info("Conversion finished successfully. %d datasets processed.", stats.found_datasets) except Exception as e: # pylint: disable=broad-exception-caught logger.critical("Fatal error during conversion process: %s", e, exc_info=True) diff --git a/middleware/sql_to_arc/src/middleware/sql_to_arc/mapper.py b/middleware/sql_to_arc/src/middleware/sql_to_arc/mapper.py index cc24f07..662baa7 100644 --- a/middleware/sql_to_arc/src/middleware/sql_to_arc/mapper.py +++ b/middleware/sql_to_arc/src/middleware/sql_to_arc/mapper.py @@ -1,11 +1,9 @@ """Mapper module to convert database rows to ARCTRL objects.""" -import json -import logging from datetime import datetime -from typing import Any, cast +from typing import Any -from arctrl import ( # type: ignore[import-untyped] +from arctrl import ( ArcAssay, ArcInvestigation, ArcStudy, @@ -14,136 +12,129 @@ Publication, ) -logger = logging.getLogger(__name__) +from middleware.sql_to_arc.models import ( + AssayRow, + ContactRow, + InvestigationRow, + PublicationRow, + StudyRow, +) + +# name=term, tan=uri (TermAccessionNumber), tsr="" (TermSourceREF - we don't have it, maybe version?) +# Spec says version is used. If we don't have TSR, we can leave it empty. + + +def _make_oa(term: str | None, uri: str | None, _version: str | None) -> OntologyAnnotation: + if not term: + return OntologyAnnotation() + # name=term, tan=uri (TermAccessionNumber), tsr="" (TermSourceREF - we don't have it, maybe version?) + # Spec says version is used. If we don't have TSR, we can leave it empty. + return OntologyAnnotation(name=term, tan=uri or "", tsr="") -def map_investigation(row: dict[str, Any]) -> ArcInvestigation: - """Map a database row to an ArcInvestigation object. - Args: - row: Dictionary containing investigation data from DB +def _format_date(d: Any) -> str | None: + """Format dates as ISO strings.""" + if isinstance(d, datetime): + return d.isoformat() + if isinstance(d, str): + return d + return None - Returns: - ArcInvestigation object - """ + +def map_investigation(row: InvestigationRow) -> ArcInvestigation: + """Map a database row to an ArcInvestigation object.""" # Handle potential None values for dates - submission_date = cast(datetime, row.get("submission_time")).isoformat() if row.get("submission_time") else None - public_release_date = cast(datetime, row.get("release_time")).isoformat() if row.get("release_time") else None + submission_date = row.submission_date + public_release_date = row.public_release_date - # Validate ID (mandatory per DB view spec, but we enforce it here to be safe) - identifier = str(row["id"]) if row.get("id") is not None else "" + identifier = row.identifier if not identifier.strip(): - raise ValueError(f"Investigation ID cannot be empty (row={row})") + # It's a required field + # But we might start empty + pass - return ArcInvestigation.create( + inv = ArcInvestigation.create( identifier=identifier, - title=row.get("title", ""), - description=row.get("description", ""), - submission_date=submission_date, - public_release_date=public_release_date, + title=row.title, + description=row.description_text, + submission_date=_format_date(submission_date), + public_release_date=_format_date(public_release_date), ) + return inv -def map_study(row: dict[str, Any]) -> ArcStudy: - """Map a database row to an ArcStudy object. +def map_study(row: StudyRow) -> ArcStudy: + """Map a database row to an ArcStudy object.""" + submission_date = row.submission_date + public_release_date = row.public_release_date - Args: - row: Dictionary containing study data from DB + return ArcStudy.create( + identifier=row.identifier, + title=row.title, + description=row.description_text, + submission_date=_format_date(submission_date), + public_release_date=_format_date(public_release_date), + ) - Returns: - ArcStudy object - """ - # Handle potential None values for dates - submission_date = cast(datetime, row.get("submission_time")).isoformat() if row.get("submission_time") else None - public_release_date = cast(datetime, row.get("release_time")).isoformat() if row.get("release_time") else None - return ArcStudy.create( - identifier=str(row["id"]), - title=row.get("title", ""), - description=row.get("description", ""), - submission_date=submission_date, - public_release_date=public_release_date, +def map_assay(row: AssayRow) -> ArcAssay: + """Map a database row to an ArcAssay object.""" + assay = ArcAssay.create( + identifier=row.identifier, + measurement_type=_make_oa(row.measurement_type_term, row.measurement_type_uri, None), + technology_type=_make_oa(row.technology_type_term, row.technology_type_uri, None), + technology_platform=_make_oa( + row.technology_platform, # Spec says platform is text but mapping to OA is allowed + None, + None, + ) + if row.technology_platform + else None, ) + return assay -def map_assay(row: dict[str, Any]) -> ArcAssay: - """Map a database row to an ArcAssay object. - Args: - row: Dictionary containing assay data from DB +def map_publication(row: PublicationRow) -> Publication: + """Map a database row to a Publication object.""" + # Publication(doi, pubMedID, authors, title, status) - Returns: - ArcAssay object + status = _make_oa(row.status_term, row.status_uri, None) - Note: - TODO: Currently measurement_type and technology_type from DB are simple strings, - but ArcAssay expects OntologyTerm objects. Once the database schema is updated to - provide full ontology information (term accession, ontology name, etc.), these - should be converted to proper OntologyTerm objects instead of being omitted. - """ - # TODO: Convert measurement_type and technology_type to OntologyTerms - # once the database provides the necessary ontology information - return ArcAssay.create( - identifier=str(row["id"]), + return Publication( + doi=row.doi, + pub_med_id=row.pubmed_id, + authors=row.authors, + title=row.title, + status=status, ) -def map_contact(row: dict[str, Any]) -> Person: - """Map a database row to a Person object. - - Args: - row: Dictionary containing contact data from DB +def map_contact(row: ContactRow) -> Person: + """Map a database row to a Person object.""" + # Person(lastName, firstName, midInitials, email, phone, fax, address, affiliation, roles) - Returns: - Person object - """ + # row.roles is now already a list (it was Json[JsonList] and validated/parsed by Pydantic) roles = [] - if row.get("roles"): - try: - roles_data = json.loads(row["roles"]) - if isinstance(roles_data, list): - for r in roles_data: - roles.append( - OntologyAnnotation( - name=r.get("term"), - tsr=r.get("version"), - tan=r.get("uri"), - ) - ) - except (json.JSONDecodeError, TypeError) as e: - # Fallback for invalid JSON or type mismatch, with logging - logger.warning( - "Could not parse roles JSON for contact with email '%s'. Error: %s", - row.get("email", "N/A"), - e, - ) - pass - - return Person.create( - first_name=row.get("first_name"), - last_name=row.get("last_name"), - mid_initials=row.get("mid_initials"), - email=row.get("email"), - phone=row.get("phone"), - fax=row.get("fax"), - address=row.get("address"), - affiliation=row.get("affiliation"), - roles=roles if roles else None, + if row.roles: + for r in row.roles: + if isinstance(r, dict): + roles.append(_make_oa(r.get("term"), r.get("uri"), r.get("version"))) + + return Person( + last_name=row.last_name, + first_name=row.first_name, + mid_initials=row.mid_initials, + email=row.email, + phone=row.phone, + fax=row.fax, + address=row.postal_address, + affiliation=row.affiliation, + roles=roles, ) -def map_publication(row: dict[str, Any]) -> Publication: - """Map a database row to a Publication object. - - Args: - row: Dictionary containing publication data from DB - - Returns: - Publication object - """ - return Publication.create( - pub_med_id=row.get("pub_med_id"), - doi=row.get("doi"), - authors=row.get("authors"), - title=row.get("title"), - ) +def map_annotation(row: dict[str, Any]) -> dict[str, Any]: + """Return raw dict for annotation processing.""" + return row diff --git a/middleware/sql_to_arc/src/middleware/sql_to_arc/models.py b/middleware/sql_to_arc/src/middleware/sql_to_arc/models.py new file mode 100644 index 0000000..fab4d65 --- /dev/null +++ b/middleware/sql_to_arc/src/middleware/sql_to_arc/models.py @@ -0,0 +1,165 @@ +"""Data models for the SQL-to-ARC conversion process.""" + +import logging +from datetime import datetime +from typing import Any, ClassVar + +from pydantic import BaseModel, ConfigDict, Field, Json, model_validator +from pydantic_core import PydanticUndefined + +logger = logging.getLogger(__name__) + +# JSON types representing the expected structure after parsing +type JsonList = list[Any] + + +def spec_field( + *, + required: bool | None = None, + allow_spec_override: bool = False, + default: Any = PydanticUndefined, + **kwargs: Any, +) -> Any: + """Define database-mapped fields with ARC spec metadata.""" + # We store the explicitly provided value (True, False, or None) + # The model validator will infer the value if it stays None + return Field( + default=default, + json_schema_extra={ + "spec_required": required, + "spec_override": allow_spec_override, + }, + **kwargs, + ) + + +class MissingRequiredColumnsError(ValueError): + """Raised when required database columns are missing for a row model.""" + + def __init__(self, model_name: str, columns: list[str]) -> None: + """Initialize exception with model name and missing required columns.""" + self.model_name = model_name + self.columns = columns + super().__init__(f'Missing required columns for "{model_name}": {", ".join(columns)}') + + +class RequiredColumnsNullError(ValueError): + """Raised when required database columns contain NULL values for a row model.""" + + def __init__(self, model_name: str, columns: list[str]) -> None: + """Initialize exception with model name and required NULL columns.""" + self.model_name = model_name + self.columns = columns + super().__init__(f'Required columns contain NULL for "{model_name}": {", ".join(columns)}') + + +class BaseRow(BaseModel): + """Base model for database rows with centralized configuration.""" + + __view_name__: ClassVar[str] = "" + + model_config = ConfigDict(extra="allow", coerce_numbers_to_str=True) + + @model_validator(mode="before") + @classmethod + def apply_spec_overrides(cls, data: Any) -> Any: + """Replace NULL (None) with default values for fields that allow spec overrides.""" + if not isinstance(data, dict): + return data + + for field_name, field_info in cls.model_fields.items(): + # Check if value is explicitly None (SQL NULL) + if data.get(field_name) is None: + json_extra = field_info.json_schema_extra + allow_override = json_extra.get("spec_override", False) if isinstance(json_extra, dict) else False + + # If override is allowed, replace with the field's default value + if allow_override: + # Only apply if a default exists + if field_info.default is not PydanticUndefined: + data[field_name] = field_info.default + elif field_info.get_default(call_default_factory=True) is not None: + # Pydantic's get_default handles factory calls safely + data[field_name] = field_info.get_default(call_default_factory=True) + + return data + + +class InvestigationRow(BaseRow): + """Pydantic model for investigation database rows.""" + + __view_name__: ClassVar[str] = "vInvestigation" + + identifier: str = spec_field() + title: str = spec_field() + description_text: str = spec_field(default="", allow_spec_override=True) + submission_date: datetime | None = spec_field(default=None) + public_release_date: datetime | None = spec_field(default=None) + + +class StudyRow(BaseRow): + """Pydantic model for study database rows.""" + + __view_name__: ClassVar[str] = "vStudy" + + identifier: str = spec_field() + investigation_ref: str = spec_field() + title: str = spec_field() + description_text: str | None = spec_field(default=None) + submission_date: datetime | None = spec_field(default=None) + public_release_date: datetime | None = spec_field(default=None) + + +class AssayRow(BaseRow): + """Pydantic model for assay database rows.""" + + __view_name__: ClassVar[str] = "vAssay" + + identifier: str = spec_field() + investigation_ref: str = spec_field() + study_ref: Json[JsonList] | None = spec_field(default=None) + title: str | None = spec_field(default=None) + description_text: str | None = spec_field(default=None) + measurement_type_term: str | None = spec_field(default=None) + measurement_type_uri: str | None = spec_field(default=None) + measurement_type_version: str | None = spec_field(default=None) + technology_type_term: str | None = spec_field(default=None) + technology_type_uri: str | None = spec_field(default=None) + technology_type_version: str | None = spec_field(default=None) + technology_platform: str | None = spec_field(default=None) + + +class PublicationRow(BaseRow): + """Pydantic model for publication database rows.""" + + __view_name__: ClassVar[str] = "vPublication" + + investigation_ref: str = spec_field() + target_type: str = spec_field() + pubmed_id: str | None = spec_field(default=None) + doi: str | None = spec_field(default=None) + authors: str | None = spec_field(default=None) + title: str | None = spec_field(default=None) + status_term: str | None = spec_field(default=None) + status_uri: str | None = spec_field(default=None) + status_version: str | None = spec_field(default=None) + target_ref: str | None = spec_field(default=None) + + +class ContactRow(BaseRow): + """Pydantic model for contact database rows.""" + + __view_name__: ClassVar[str] = "vContact" + + investigation_ref: str = spec_field() + target_type: str = spec_field() + last_name: str | None = spec_field(default=None) + first_name: str | None = spec_field(default=None) + mid_initials: str | None = spec_field(default=None) + email: str | None = spec_field(default=None) + phone: str | None = spec_field(default=None) + fax: str | None = spec_field(default=None) + postal_address: str | None = spec_field(default=None) + affiliation: str | None = spec_field(default=None) + roles: Json[JsonList] | None = spec_field(default=None) + target_ref: str | None = spec_field(default=None) diff --git a/middleware/sql_to_arc/src/middleware/sql_to_arc/processor.py b/middleware/sql_to_arc/src/middleware/sql_to_arc/processor.py new file mode 100644 index 0000000..46bb859 --- /dev/null +++ b/middleware/sql_to_arc/src/middleware/sql_to_arc/processor.py @@ -0,0 +1,299 @@ +"""Orchestration and worker management for the SQL-to-ARC conversion process.""" + +import asyncio +import concurrent.futures +import json +import logging +import multiprocessing +from collections import defaultdict +from collections.abc import AsyncGenerator +from dataclasses import dataclass +from typing import Any, TypeVar + +from opentelemetry import trace +from pydantic import BaseModel + +from middleware.api_client import ApiClient, ApiClientError +from middleware.sql_to_arc.builder import build_single_arc_task +from middleware.sql_to_arc.config import Config +from middleware.sql_to_arc.context import ( + ArcBuildData, + RelatedDataBatch, + WorkerContext, +) +from middleware.sql_to_arc.database import Database +from middleware.sql_to_arc.models import InvestigationRow +from middleware.sql_to_arc.stats import ProcessingStats + +logger = logging.getLogger(__name__) + +T = TypeVar("T", bound=BaseModel) + + +async def _upload_and_update_stats( + ctx: WorkerContext, + arc_json: str, + investigation_id: str, + stats: ProcessingStats, + inv_info: str, +) -> None: + """Upload ARC and update statistics.""" + tracer = trace.get_tracer(__name__) + try: + with tracer.start_as_current_span( + "upload_arc", attributes={"rdi": ctx.rdi, "worker_id": ctx.worker_id, "investigation_id": investigation_id} + ): + # Parse JSON back to dict for the API client (it will serialize again, + # but we need the dict for validation/processing) + arc_dict = json.loads(arc_json) + + await ctx.client.create_or_update_arc( + rdi=ctx.rdi, + arc=arc_dict, + ) + + logger.info("%s: Upload request finished. API reported success for ARC %s.", inv_info, investigation_id) + + except (ConnectionError, TimeoutError, ApiClientError) as e: + logger.error("%s: Failed to upload ARC %s: %s", inv_info, investigation_id, e, exc_info=True) + stats.failed_datasets += 1 + stats.failed_ids.append(investigation_id) + + +async def _build_and_upload_single_arc( + ctx: WorkerContext, + investigation: InvestigationRow, + *, + stats: ProcessingStats, + inv_info: str, + semaphore: asyncio.Semaphore, +) -> None: + """Build a single ARC and upload it.""" + inv_id = str(investigation.identifier) + # Acquire semaphore to limit concurrency + async with semaphore: + # Prepare data bundle for this investigation + studies = ctx.studies_by_inv.get(inv_id, []) + assays = ctx.assays_by_inv.get(inv_id, []) + + if assays and not studies: + logger.warning( + "%s: Investigation %s has assays but no studies. This is allowed but unusual.", inv_info, inv_id + ) + + build_data = ArcBuildData( + investigation_row=investigation, + studies=studies, + assays=assays, + contacts=ctx.contacts_by_inv.get(inv_id, []), + publications=ctx.pubs_by_inv.get(inv_id, []), + annotations=ctx.anns_by_inv.get(inv_id, []), + ) + + # Build ARC in executor + loop = asyncio.get_event_loop() + try: + # Replaced direct ARC transfer with JSON transfer from worker + # Note: build_single_arc_task now returns a JSON string + arc_json = await asyncio.wait_for( + loop.run_in_executor(ctx.executor, build_single_arc_task, build_data), + timeout=getattr(ctx, "arc_generation_timeout_minutes", 30) * 60, + ) + + if arc_json is None: + logger.error("%s: Build returned None for investigation %s", inv_info, inv_id) + stats.failed_datasets += 1 + stats.failed_ids.append(inv_id) + return + + json_size_kb = len(arc_json.encode("utf-8")) / 1024 + logger.info("%s: ARC JSON created: size=%.2fKB", inv_info, json_size_kb) + + # Upload single ARC + await _upload_and_update_stats(ctx, arc_json, inv_id, stats, inv_info) + + except TimeoutError: + logger.error("%s: ARC generation timed out for investigation %s", inv_info, inv_id) + stats.failed_datasets += 1 + stats.failed_ids.append(inv_id) + except (ValueError, RuntimeError) as e: + logger.error("%s: Failed to build ARC for investigation %s: %s", inv_info, inv_id, e) + stats.failed_datasets += 1 + stats.failed_ids.append(inv_id) + + +async def process_investigation( + ctx: WorkerContext, + investigation: InvestigationRow, + stats: ProcessingStats, + inv_info: str, + semaphore: asyncio.Semaphore, +) -> None: + """Process a single investigation.""" + tracer = trace.get_tracer(__name__) + inv_id = str(investigation.identifier) + + with tracer.start_as_current_span( + "build_investigation", + attributes={"investigation_id": inv_id, "worker_id": ctx.worker_id}, + ): + logger.info("%s: Building ARC for investigation %s...", inv_info, inv_id) + await _build_and_upload_single_arc( + ctx, + investigation, + stats=stats, + inv_info=inv_info, + semaphore=semaphore, + ) + + +async def _fetch_and_group_related_data(db: Database, investigation_ids: list[str]) -> RelatedDataBatch: + """Fetch related data in bulk and group by investigation ID.""" + logger.info("Fetching related data (studies, assays, contacts, etc.)...") + + async def group_stream( + gen: AsyncGenerator[Any, None], + ) -> tuple[dict[str, list[Any]], int]: + m = defaultdict(list) + count = 0 + async for r in gen: + # All models and the annotation dict have investigation_ref + inv_ref = r["investigation_ref"] if isinstance(r, dict) else r.investigation_ref + m[str(inv_ref)].append(r) + count += 1 + return dict(m), count + + studies_by_inv, study_count = await group_stream(db.stream_studies(investigation_ids)) + assays_by_inv, assay_count = await group_stream(db.stream_assays(investigation_ids)) + contacts_by_inv, _ = await group_stream(db.stream_contacts(investigation_ids)) + pubs_by_inv, _ = await group_stream(db.stream_publications(investigation_ids)) + anns_by_inv, _ = await group_stream(db.stream_annotation_tables(investigation_ids)) + + return RelatedDataBatch( + studies_by_inv=studies_by_inv, + assays_by_inv=assays_by_inv, + contacts_by_inv=contacts_by_inv, + pubs_by_inv=pubs_by_inv, + anns_by_inv=anns_by_inv, + study_count=study_count, + assay_count=assay_count, + ) + + +@dataclass(slots=True) +class WorkerResources: + """Orchestration resources shared across investigation tasks.""" + + client: ApiClient + config: Config + stats: ProcessingStats + executor: concurrent.futures.Executor + semaphore: asyncio.Semaphore + + +def _spawn_investigation_task( + investigation: InvestigationRow, + idx: int, + batch_data: RelatedDataBatch, + res: WorkerResources, + running_tasks: set[asyncio.Task[None]], +) -> None: + """Create worker context and spawn a processing task.""" + ctx = WorkerContext( + client=res.client, + rdi=res.config.rdi, + studies_by_inv=batch_data.studies_by_inv, + assays_by_inv=batch_data.assays_by_inv, + contacts_by_inv=batch_data.contacts_by_inv, + pubs_by_inv=batch_data.pubs_by_inv, + anns_by_inv=batch_data.anns_by_inv, + worker_id=idx % res.config.max_concurrent_arc_builds, + total_workers=res.config.max_concurrent_arc_builds, + executor=res.executor, + arc_generation_timeout_minutes=res.config.arc_generation_timeout_minutes, + ) + + inv_info = f"Investigation {idx}" + task = asyncio.create_task(process_investigation(ctx, investigation, res.stats, inv_info, res.semaphore)) + running_tasks.add(task) + task.add_done_callback(running_tasks.discard) + + +async def process_investigations( + db: Database, + client: ApiClient, + config: Config, +) -> ProcessingStats: + """Fetch investigations from DB and process them concurrently with flow control.""" + stats = ProcessingStats() + semaphore = asyncio.Semaphore(config.max_concurrent_tasks) + + logger.info( + "Starting SQL-to-ARC processing: CPU_workers=%d, Max_tasks=%d, Batch_size=%d", + config.max_concurrent_arc_builds, + config.max_concurrent_tasks, + config.db_batch_size, + ) + + with ( + concurrent.futures.ProcessPoolExecutor( + max_workers=config.max_concurrent_arc_builds, + mp_context=multiprocessing.get_context("spawn"), + ) as executor, + trace.get_tracer(__name__).start_as_current_span("process_investigations"), + ): + running_tasks: set[asyncio.Task[None]] = set() + inv_idx = 0 + investigation_gen = db.stream_investigations(stats=stats, limit=config.debug_limit) + + while True: + batch = [] + try: + for _ in range(config.db_batch_size): + try: + batch.append(await anext(investigation_gen)) + except StopAsyncIteration: + break + except (RuntimeError, OSError, ConnectionError) as e: + logger.error("Database or connection error while fetching investigations: %s", e, exc_info=True) + break + except Exception as e: + logger.error("Unexpected error while fetching investigations: %s", e, exc_info=True) + raise + + if not batch: + break + + if len(running_tasks) >= config.max_concurrent_tasks: + await asyncio.wait(running_tasks, return_when=asyncio.FIRST_COMPLETED) + + # 3. Relational Batching: Fetch all related data for this batch at once + batch_data = await _fetch_and_group_related_data(db, [str(inv.identifier) for inv in batch]) + stats.total_studies += batch_data.study_count + stats.total_assays += batch_data.assay_count + + # 4. Prepare resources for spawning tasks + res = WorkerResources( + client=client, + config=config, + stats=stats, + executor=executor, + semaphore=semaphore, + ) + + # 5. Spawn tasks for each investigation in the batch + for investigation in batch: + inv_idx += 1 + _spawn_investigation_task( + investigation, + inv_idx, + batch_data, + res, + running_tasks, + ) + + if running_tasks: + logger.info("Waiting for %d remaining tasks to complete...", len(running_tasks)) + await asyncio.gather(*running_tasks) + + return stats diff --git a/middleware/sql_to_arc/src/middleware/sql_to_arc/py.typed b/middleware/sql_to_arc/src/middleware/sql_to_arc/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/middleware/sql_to_arc/src/middleware/sql_to_arc/stats.py b/middleware/sql_to_arc/src/middleware/sql_to_arc/stats.py new file mode 100644 index 0000000..80ee557 --- /dev/null +++ b/middleware/sql_to_arc/src/middleware/sql_to_arc/stats.py @@ -0,0 +1,77 @@ +"""Statistics tracking for the conversion process.""" + +import json + +from pydantic import BaseModel, ConfigDict + + +class ProcessingStats(BaseModel): + """Statistics for the conversion process.""" + + found_datasets: int = 0 + total_studies: int = 0 + total_assays: int = 0 + failed_datasets: int = 0 + failed_ids: list[str] = [] + duration_seconds: float = 0.0 + + model_config = ConfigDict(arbitrary_types_allowed=True) + + def merge(self, other: "ProcessingStats") -> None: + """Merge another stats object into this one.""" + self.found_datasets += other.found_datasets + self.failed_datasets += other.failed_datasets + self.failed_ids.extend(other.failed_ids) + # Note: total_studies, total_assays are counted centrally, not merged from workers + + def to_jsonld(self, rdi_identifier: str | None = None, rdi_url: str | None = None) -> str: + """Return JSON-LD representation of stats using Schema.org and PROV terms.""" + # Convert duration to ISO 8601 duration format (PTx.xS) + duration_iso = f"PT{self.duration_seconds:.2f}S" + + ld_struct = { + "@context": { + "schema": "http://schema.org/", + "prov": "http://www.w3.org/ns/prov#", + "void": "http://rdfs.org/ns/void#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + # Map duration to schema:duration (Expects ISO 8601 string) + "duration": {"@id": "schema:duration", "@type": "schema:Duration"}, + # Map failed IDs to schema:error (list of strings) + "failed_ids": {"@id": "schema:error", "@container": "@set"}, + # Map status + "status": {"@id": "schema:actionStatus"}, + # Use VoID for counts (statistic items) + "found_datasets": {"@id": "void:entities", "@type": "xsd:integer"}, + # Custom descriptive terms for study/assay counts as they are domain specific + # We map them to schema:result for semantics, but keep key names + "total_studies": {"@id": "schema:result", "@type": "xsd:integer"}, + "total_assays": {"@id": "schema:result", "@type": "xsd:integer"}, + }, + "@type": ["prov:Activity", "schema:CreateAction"], + "schema:name": "SQL to ARC Conversion Run", + "schema:instrument": { + "@type": "schema:SoftwareApplication", + "schema:name": "FAIRagro Middleware SQL-to-ARC", + }, + # Process status + "status": "schema:CompletedActionStatus" if self.failed_datasets == 0 else "schema:FailedActionStatus", + # Metrics + "duration": duration_iso, + "duration_seconds": round(self.duration_seconds, 2), # Keep raw float for easy parsing + "found_datasets": self.found_datasets, + "total_studies": self.total_studies, + "total_assays": self.total_assays, + "failed_datasets": self.failed_datasets, + "failed_ids": sorted(self.failed_ids), + } + + if rdi_identifier and rdi_url: + ld_struct["prov:used"] = { + "@id": rdi_url, + "@type": "schema:Organization", # RDI acts as an Organization/Service + "schema:identifier": rdi_identifier, + "schema:name": f"Research Data Infrastructure: {rdi_identifier}", + } + + return json.dumps(ld_struct, indent=2) diff --git a/middleware/sql_to_arc/tests/integration/test_workflow.py b/middleware/sql_to_arc/tests/integration/test_workflow.py index 1142504..0cb1151 100644 --- a/middleware/sql_to_arc/tests/integration/test_workflow.py +++ b/middleware/sql_to_arc/tests/integration/test_workflow.py @@ -1,17 +1,39 @@ """Integration tests for the SQL-to-ARC workflow.""" import asyncio -import multiprocessing -from concurrent.futures import ProcessPoolExecutor +import json +from collections.abc import AsyncGenerator +from concurrent.futures import ThreadPoolExecutor from typing import Any from unittest.mock import AsyncMock, MagicMock import pytest +from arctrl import ARC -from middleware.api_client import ApiClient -from middleware.shared.api_models.models import CreateOrUpdateArcsResponse +from middleware.api_client import ApiClient, ArcMetadata, ArcResult, ArcStatus +from middleware.api_client.models import ArcLifecycleStatus from middleware.shared.config.config_base import OtelConfig -from middleware.sql_to_arc.main import DatasetContext, ProcessingStats, WorkerContext, main, process_single_dataset +from middleware.sql_to_arc.config import Config +from middleware.sql_to_arc.context import WorkerContext +from middleware.sql_to_arc.main import main +from middleware.sql_to_arc.models import ( + AssayRow, + ContactRow, + InvestigationRow, + PublicationRow, + StudyRow, +) +from middleware.sql_to_arc.processor import process_investigation +from middleware.sql_to_arc.stats import ProcessingStats + + +class MockExecutor(ThreadPoolExecutor): + """Mock ThreadPoolExecutor to prevent multiprocessing.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the mock executor.""" + kwargs.pop("mp_context", None) + super().__init__(*args, **kwargs) @pytest.fixture @@ -39,185 +61,815 @@ def mock_db_connection(mock_db_cursor: AsyncMock) -> AsyncMock: def mock_api_client() -> AsyncMock: """Mock API client.""" client = AsyncMock(spec=ApiClient) - client.create_or_update_arc.return_value = CreateOrUpdateArcsResponse( - client_id="test", - message="success", - rdi="test", - arcs=[], + client.create_or_update_arc.return_value = ArcResult( + arc_id="test", + status=ArcStatus.CREATED, + metadata=ArcMetadata( + arc_hash="", + status=ArcLifecycleStatus.ACTIVE, + first_seen="2026-01-01T00:00:00Z", + last_seen="2026-01-01T00:00:00Z", + ), ) return client +class WorkflowTester: + """Helper class to simplify integration tests for sql_to_arc.""" + + def __init__(self, mocker: MagicMock, mock_api_client: AsyncMock) -> None: + """ + Initialize the WorkflowTester with mock dependencies. + + Args: + mocker (MagicMock): Mocking utility for patching dependencies. + mock_api_client (AsyncMock): Mocked API client for simulating API interactions. + """ + self.mocker = mocker + self.api_client = mock_api_client + self.db = AsyncMock() + self.db.validate_schema = AsyncMock(return_value=None) + self.db.to_jsonld.return_value = "{}" + self.captured_arcs: list[ARC] = [] + + # Default empty mocks + self.set_db_content() + + # Patch Database class + mocker.patch("middleware.sql_to_arc.main.Database", return_value=self.db) + + # Patch API Client context manager + mocker.patch( + "middleware.sql_to_arc.main.ApiClient", + return_value=AsyncMock(__aenter__=AsyncMock(return_value=self.api_client)), + ) + + # Patch configuration + self.mock_config = MagicMock(spec=Config) + self.mock_config.api_client = MagicMock() + self.mock_config.rdi = "test-rdi" + self.mock_config.rdi_url = "http://test.com" + self.mock_config.max_concurrent_arc_builds = 1 + self.mock_config.max_concurrent_tasks = 4 + self.mock_config.db_batch_size = 10 + self.mock_config.arc_generation_timeout_minutes = 30 + self.mock_config.debug_limit = None + self.mock_config.log_level = "INFO" + self.mock_config.otel = OtelConfig(endpoint=None, log_console_spans=False, log_level="INFO") + mock_conn = MagicMock() + mock_conn.get_secret_value.return_value = "sqlite+aiosqlite:///:memory:" + self.mock_config.connection_string = mock_conn + + mocker.patch("middleware.sql_to_arc.main.ConfigWrapper.from_yaml_file") + mocker.patch("middleware.sql_to_arc.main.Config.from_config_wrapper", return_value=self.mock_config) + mocker.patch("middleware.sql_to_arc.main.configure_logging") + + # Capture ARCs on API call + async def capture_arc(rdi: str, arc: Any) -> ArcResult: + serialized_arc = arc + if isinstance(arc, dict): + # Convert back to ARC object for test compatibility + # processor.py sends a dict, but legacy tests expect an ARC object + serialized_arc = ARC.from_rocrate_json_string(json.dumps(arc)) + + self.captured_arcs.append(serialized_arc) + return ArcResult( + arc_id=rdi, + status=ArcStatus.CREATED, + metadata=ArcMetadata( + arc_hash="", + status=ArcLifecycleStatus.ACTIVE, + first_seen="2026-01-01T00:00:00Z", + last_seen="2026-01-01T00:00:00Z", + ), + ) + + self.api_client.create_or_update_arc.side_effect = capture_arc + + @staticmethod + def _as_gen(data: list[dict[str, Any]], model_cls: type[Any] | None = None) -> AsyncGenerator[Any, None]: + async def gen() -> AsyncGenerator[Any, None]: + for item in data: + yield model_cls.model_validate(item) if model_cls else item + + return gen() + + def set_db_content( # noqa: PLR0913, PLR0917 + self, + investigations: list[dict[str, Any]] | None = None, + studies: list[dict[str, Any]] | None = None, + assays: list[dict[str, Any]] | None = None, + contacts: list[dict[str, Any]] | None = None, + publications: list[dict[str, Any]] | None = None, + annotations: list[dict[str, Any]] | None = None, + ) -> None: + """Mock the database streaming methods with provided data.""" + + def _prepare_data(data: list[dict[str, Any]] | None, target_cls: type[Any] | None) -> list[dict[str, Any]]: + if not data or not target_cls: + return data or [] + prepared = [] + model_fields = target_cls.model_fields.keys() + for item in data: + new_item = item.copy() + # Rename description to description_text if needed + if "description" in new_item and "description_text" in model_fields: + new_item["description_text"] = new_item.pop("description") + # Add default values for required fields missing in test data + for field_name, field_info in target_cls.model_fields.items(): + extra = field_info.json_schema_extra + is_required = isinstance(extra, dict) and extra.get("spec_required") + if is_required and field_name not in new_item: + new_item[field_name] = "Test Value" + prepared.append(new_item) + return prepared + + # The stream_* methods are async generator methods (not coroutines), so they must + # be set as regular MagicMock, not AsyncMock. AsyncMock would wrap the return + # value in a coroutine, but async generators are called directly (no await) and + # return an AsyncGenerator object immediately. + self.db.stream_investigations = MagicMock( + side_effect=lambda *args, **kwargs: self._as_gen( # noqa: ARG005 + _prepare_data(investigations, InvestigationRow), InvestigationRow + ) + ) + self.db.stream_studies = MagicMock( + side_effect=lambda *args, **kwargs: self._as_gen( # noqa: ARG005 + _prepare_data(studies, StudyRow), StudyRow + ) + ) + self.db.stream_assays = MagicMock( + side_effect=lambda *args, **kwargs: self._as_gen( # noqa: ARG005 + _prepare_data(assays, AssayRow), AssayRow + ) + ) + self.db.stream_contacts = MagicMock( + side_effect=lambda *args, **kwargs: self._as_gen( # noqa: ARG005 + _prepare_data(contacts, ContactRow), ContactRow + ) + ) + self.db.stream_publications = MagicMock( + side_effect=lambda *args, **kwargs: self._as_gen( # noqa: ARG005 + _prepare_data(publications, PublicationRow), PublicationRow + ) + ) + self.db.stream_annotation_tables = MagicMock( + side_effect=lambda *args, **kwargs: self._as_gen( # noqa: ARG005 + annotations or [] + ) + ) + + async def run(self) -> list[ARC]: + """Execute the main workflow and return captured ARC objects.""" + # Prevent real engine creation + self.mocker.patch("sqlalchemy.ext.asyncio.create_async_engine", return_value=MagicMock()) + self.mocker.patch( + "sqlalchemy.ext.asyncio.AsyncSession", + return_value=AsyncMock(__aenter__=AsyncMock(return_value=AsyncMock())), + ) + self.mocker.patch("middleware.sql_to_arc.processor.concurrent.futures.ProcessPoolExecutor", MockExecutor) + + await main(["-c", "config.yaml"]) + return self.captured_arcs + + +@pytest.fixture +def workflow_tester(mocker: MagicMock, mock_api_client: AsyncMock) -> WorkflowTester: + """Fixture providing a WorkflowTester instance.""" + return WorkflowTester(mocker, mock_api_client) + + @pytest.mark.asyncio -async def test_process_single_dataset(mock_api_client: AsyncMock) -> None: - """Test single dataset processing.""" +async def test_process_worker_investigations(mock_api_client: AsyncMock) -> None: + """Test worker investigations processing.""" investigation_rows: list[dict[str, Any]] = [ - {"id": 1, "title": "Test 1", "description": "Desc 1", "submission_time": None, "release_time": None}, - {"id": 2, "title": "Test 2", "description": "Desc 2", "submission_time": None, "release_time": None}, + { + "identifier": 1, + "title": "Test 1", + "description_text": "Desc 1", + "submission_time": None, + "release_time": None, + }, + { + "identifier": 2, + "title": "Test 2", + "description_text": "Desc 2", + "submission_time": None, + "release_time": None, + }, ] - studies_by_investigation: dict[int, list[dict[str, Any]]] = {1: [], 2: []} - assays_by_study: dict[int, list[dict[str, Any]]] = {} - - mp_context = multiprocessing.get_context("spawn") - with ProcessPoolExecutor(max_workers=5, mp_context=mp_context) as executor: + studies_by_investigation: dict[str, list[StudyRow]] = { + "1": [StudyRow.model_validate(study) for study in list[dict[str, Any]]()], + "2": [StudyRow.model_validate(study) for study in list[dict[str, Any]]()], + } + assays_by_study: dict[str, list[dict[str, Any]]] = {} + with ThreadPoolExecutor(max_workers=5) as executor: ctx = WorkerContext( client=mock_api_client, rdi="edaphobase", + studies_by_inv={ + key: [StudyRow.model_validate(study) for study in value] + for key, value in studies_by_investigation.items() + }, + assays_by_inv={ + key: [AssayRow.model_validate(assay) for assay in value] for key, value in assays_by_study.items() + }, + contacts_by_inv={}, + pubs_by_inv={}, + anns_by_inv={}, + worker_id=1, + total_workers=1, executor=executor, - max_studies=5000, - max_assays=10000, - arc_generation_timeout_minutes=60, ) semaphore = asyncio.Semaphore(1) stats = ProcessingStats() - - for inv in investigation_rows: - # Build a minimal DatasetContext as expected by process_single_dataset - dataset_context = DatasetContext( - investigation_row=inv, - studies=studies_by_investigation.get(inv["id"], []), - assays_by_study=assays_by_study, - ) - # Call with correct arguments: ctx, dataset_context, semaphore, stats - await process_single_dataset(ctx, dataset_context, semaphore, stats) + for i, inv in enumerate(investigation_rows): + inv_info = f"Investigation {i + 1}" + await process_investigation(ctx, InvestigationRow.model_validate(inv), stats, inv_info, semaphore) assert mock_api_client.create_or_update_arc.called + # There should be two calls, each with one ARC (since batch size is always 1) assert mock_api_client.create_or_update_arc.call_count == 2 # noqa: PLR2004 for call in mock_api_client.create_or_update_arc.call_args_list: assert call.kwargs["rdi"] == "edaphobase" - # Each call sends one ARC as dict or ARC object - assert "arc" in call.kwargs + assert isinstance(call.kwargs["arc"], dict) + assert "@graph" in call.kwargs["arc"] -@pytest.fixture -def mock_main_config(mocker: MagicMock) -> MagicMock: - """Mock configuration for main workflow.""" - config = MagicMock() - config.db_name = "test_db" - config.db_user = "test_user" - config.db_password.get_secret_value.return_value = "test_password" - config.db_host = "localhost" - config.db_port = 5432 - config.rdi = "edaphobase" - config.max_concurrent_arc_builds = 5 - config.max_concurrent_tasks = 10 - config.db_batch_size = 100 - config.api_client = MagicMock() - config.log_level = "INFO" - config.otel = OtelConfig(endpoint=None, log_console_spans=False, log_level="INFO") - config.max_studies = 5000 - config.max_assays = 10000 - config.arc_generation_timeout_minutes = 60 - config.rdi_url = "https://example.com" # Real string for JSON serialization - - mocker.patch("middleware.sql_to_arc.main.ConfigWrapper.from_yaml_file") - mocker.patch("middleware.sql_to_arc.main.Config.from_config_wrapper", return_value=config) - mocker.patch("middleware.sql_to_arc.main.configure_logging") - mocker.patch("middleware.sql_to_arc.main.initialize_tracing", return_value=(MagicMock(), MagicMock())) - return config - - -def _setup_cursor_side_effects( - mock_db_cursor: AsyncMock, investigations: list[dict], studies: list[dict], assays: list[dict] -) -> AsyncMock: - """Set up cursor behavior for bulk fetch strategy.""" - mock_detail_cursor = AsyncMock() - mock_detail_cursor.fetchall.return_value = [] - mock_db_cursor.connection = MagicMock() - mock_db_cursor.connection.cursor.return_value.__aenter__.return_value = mock_detail_cursor - - async def detail_fetchall_side_effect() -> list[dict[str, Any]]: - if not mock_detail_cursor.execute.call_args: - return [] - last_query = mock_detail_cursor.execute.call_args[0][0] - if 'FROM "ARC_Study"' in last_query: - return studies - if 'FROM "ARC_Assay"' in last_query: - return assays - return [] - - mock_detail_cursor.fetchall.side_effect = detail_fetchall_side_effect - fetchmany_done: list[bool] = [] - - async def fetchmany_side_effect(_size: int = 100) -> list[dict[str, Any]]: - _ = _size - last_query = mock_db_cursor.execute.call_args[0][0] - if 'FROM "ARC_Investigation"' in last_query and not fetchmany_done: - fetchmany_done.append(True) - return investigations - return [] - - mock_db_cursor.fetchall.side_effect = AsyncMock(return_value=[]) - mock_db_cursor.fetchmany.side_effect = fetchmany_side_effect - return mock_detail_cursor +@pytest.mark.asyncio +async def test_main_workflow(workflow_tester: WorkflowTester) -> None: + """Test the main workflow with mocked DB and API using WorkflowTester.""" + # Setup DB data + investigations = [ + {"identifier": "1", "title": "Inv 1", "description_text": "Desc 1"}, + {"identifier": "2", "title": "Inv 2", "description_text": "Desc 2"}, + ] + studies = [ + {"identifier": "10", "investigation_ref": "1", "title": "Study 1", "description_text": "Desc S1"}, + {"identifier": "11", "investigation_ref": "2", "title": "Study 2", "description_text": "Desc S2"}, + ] + assays = [ + {"identifier": "100", "study_ref": '["10"]', "investigation_ref": "1"}, + {"identifier": "101", "study_ref": '["11"]', "investigation_ref": "2"}, + ] + + workflow_tester.set_db_content(investigations=investigations, studies=studies, assays=assays) + + # Run main + arcs = await workflow_tester.run() + + # Verify results + assert len(arcs) == 2 # noqa: PLR2004 + identifiers = {arc.Identifier for arc in arcs} + assert identifiers == {"1", "2"} + + # Spot check deep property + arc1 = next(a for a in arcs if a.Identifier == "1") + assert arc1.Studies[0].Identifier == "10" + # Check if assays are present + assert len(arc1.Assays) > 0 + assert any(a.Identifier == "100" for a in arc1.Assays) @pytest.mark.asyncio -async def test_main_workflow( - mocker: MagicMock, - mock_db_connection: AsyncMock, - mock_db_cursor: AsyncMock, - mock_api_client: AsyncMock, - mock_main_config: MagicMock, -) -> None: - """Test the main workflow with mocked DB and API.""" - _ = mock_main_config - mocker.patch( - "psycopg.AsyncConnection.connect", - return_value=AsyncMock(__aenter__=AsyncMock(return_value=mock_db_connection)), +async def test_investigation_with_publications_and_contacts(workflow_tester: WorkflowTester) -> None: + """Test investigation with multiple publications and contacts at the investigation level.""" + inv_id = "INV_PUBLICATION_TEST" + investigations = [{"identifier": inv_id, "title": "Publication and Contact Test"}] + + publications = [ + { + "investigation_ref": inv_id, + "target_type": "investigation", + "title": "First Paper", + "doi": "10.1234/1", + "pubmed_id": "123456", + "authors": "Author A, Author B", + "status_term": "published", + }, + { + "investigation_ref": inv_id, + "target_type": "investigation", + "title": "Second Paper", + "doi": "10.1234/2", + "pubmed_id": "654321", + "authors": "Author C", + "status_term": "in review", + }, + ] + + contacts = [ + { + "investigation_ref": inv_id, + "target_type": "investigation", + "last_name": "Doe", + "first_name": "John", + "email": "john.doe@example.com", + "affiliation": "Institute A", + "roles": json.dumps([{"term": "Principal Investigator"}]), + }, + { + "investigation_ref": inv_id, + "target_type": "investigation", + "last_name": "Smith", + "first_name": "Jane", + "email": "jane.smith@example.com", + "affiliation": "Institute B", + "roles": json.dumps([{"term": "Data Curator"}]), + }, + ] + + workflow_tester.set_db_content( + investigations=investigations, + publications=publications, + contacts=contacts, ) - mocker.patch( - "middleware.sql_to_arc.main.ApiClient", - return_value=AsyncMock(__aenter__=AsyncMock(return_value=mock_api_client)), + + arcs = await workflow_tester.run() + + assert len(arcs) == 1 + arc = arcs[0] + assert arc.Identifier == inv_id + + # Verify Publications + assert len(arc.Publications) == 2 # noqa: PLR2004 + titles = {p.Title for p in arc.Publications} + assert titles == {"First Paper", "Second Paper"} + assert any(p.DOI == "10.1234/1" for p in arc.Publications) + + # Verify Contacts + assert len(arc.Contacts) == 2 # noqa: PLR2004 + emails = {c.EMail for c in arc.Contacts} + assert emails == {"john.doe@example.com", "jane.smith@example.com"} + assert any(c.LastName == "Doe" for c in arc.Contacts) + assert any(oa.Name == "Data Curator" for c in arc.Contacts for oa in c.Roles) + + +@pytest.mark.asyncio +async def test_study_with_publications_and_contacts(workflow_tester: WorkflowTester) -> None: + """Test study with multiple publications and contacts at the study level.""" + inv_id = "INV_S" + study_id = "STUDY_1" + + investigations = [{"identifier": inv_id, "title": "Study Level Metadata Test"}] + studies = [{"identifier": study_id, "investigation_ref": inv_id, "title": "Target Study"}] + + publications = [ + { + "investigation_ref": inv_id, + "target_type": "study", + "target_ref": study_id, + "title": "Study Specific Paper 1", + "doi": "10.1234/study.1", + }, + { + "investigation_ref": inv_id, + "target_type": "study", + "target_ref": study_id, + "title": "Study Specific Paper 2", + "doi": "10.1234/study.2", + }, + ] + + contacts = [ + { + "investigation_ref": inv_id, + "target_type": "study", + "target_ref": study_id, + "last_name": "Scientist", + "first_name": "Alice", + "email": "alice@example.com", + "roles": json.dumps([{"term": "Collaborator"}]), + }, + { + "investigation_ref": inv_id, + "target_type": "study", + "target_ref": study_id, + "last_name": "Researcher", + "first_name": "Bob", + "email": "bob@example.com", + "roles": json.dumps([{"term": "Lead Scientist"}]), + }, + ] + + workflow_tester.set_db_content( + investigations=investigations, + studies=studies, + publications=publications, + contacts=contacts, ) - # Setup DB data - invs = [ - {"id": 1, "title": "I1", "description": "D1", "submission_time": None, "release_time": None}, - {"id": 2, "title": "I2", "description": "D2", "submission_time": None, "release_time": None}, + arcs = await workflow_tester.run() + + assert len(arcs) == 1 + arc = arcs[0] + assert len(arc.Studies) == 1 + study = arc.Studies[0] + assert study.Identifier == study_id + + # Verify Study Publications + assert len(study.Publications) == 2 # noqa: PLR2004 + titles = {p.Title for p in study.Publications} + assert titles == {"Study Specific Paper 1", "Study Specific Paper 2"} + + # Verify Study Contacts + assert len(study.Contacts) == 2 # noqa: PLR2004 + emails = {c.EMail for c in study.Contacts} + assert emails == {"alice@example.com", "bob@example.com"} + + +@pytest.mark.asyncio +async def test_assay_with_contacts(workflow_tester: WorkflowTester) -> None: + """Test assay with multiple contacts (performers) at the assay level.""" + inv_id = "INV_A" + assay_id = "ASSAY_1" + + investigations = [{"identifier": inv_id, "title": "Assay Metadata Test"}] + # Assays need to be linked to studies in the DB row via study_ref if we want them registered in studies, + # but the mapper/main logic also adds them to the ARC level. + assays = [{"identifier": assay_id, "investigation_ref": inv_id}] + + contacts = [ + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "last_name": "Technician", + "first_name": "Tom", + "email": "tom@example.com", + "roles": json.dumps([{"term": "Operator"}]), + }, + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "last_name": "Analyst", + "first_name": "Anna", + "email": "anna@example.com", + "roles": json.dumps([{"term": "Data Analyst"}]), + }, ] - sts = [ + + workflow_tester.set_db_content( + investigations=investigations, + assays=assays, + contacts=contacts, + ) + + arcs = await workflow_tester.run() + + assert len(arcs) == 1 + arc = arcs[0] + assert len(arc.Assays) == 1 + assay = arc.Assays[0] + assert assay.Identifier == assay_id + + # Verify Assay Performers (contacts mapped to performers in assays) + assert len(assay.Performers) == 2 # noqa: PLR2004 + emails = {p.EMail for p in assay.Performers} + assert emails == {"tom@example.com", "anna@example.com"} + assert any(p.LastName == "Technician" for p in assay.Performers) + + +@pytest.mark.asyncio +async def test_complex_hierarchy(workflow_tester: WorkflowTester) -> None: + """Test investigation with multiple studies and assays linked to them.""" + inv_id = "INV_COMPLEX" + s1_id = "S1" + s2_id = "S2" + a1_id = "A1" + a2_id = "A2" + a3_id = "A3" + + investigations = [{"identifier": inv_id, "title": "Complex Hierarchy Test"}] + studies = [ + {"identifier": s1_id, "investigation_ref": inv_id, "title": "Study 1"}, + {"identifier": s2_id, "investigation_ref": inv_id, "title": "Study 2"}, + ] + # Assays link to studies via 'study_ref' which is a JSON list of identifiers + assays = [ + {"identifier": a1_id, "investigation_ref": inv_id, "study_ref": json.dumps([s1_id])}, + {"identifier": a2_id, "investigation_ref": inv_id, "study_ref": json.dumps([s1_id])}, + {"identifier": a3_id, "investigation_ref": inv_id, "study_ref": json.dumps([s2_id])}, + ] + + workflow_tester.set_db_content( + investigations=investigations, + studies=studies, + assays=assays, + ) + + arcs = await workflow_tester.run() + + assert len(arcs) == 1 + arc = arcs[0] + assert arc.Identifier == inv_id + + # Verify studies + assert len(arc.Studies) == 2 # noqa: PLR2004 + assert any(s.Identifier == s1_id for s in arc.Studies) + s2 = next(s for s in arc.Studies if s.Identifier == s2_id) + + # Verify assays at ARC level (RegisteredAssays link might not roundtrip in some versions) + assert any(a.Identifier == a1_id for a in arc.Assays) + assert any(a.Identifier == a2_id for a in arc.Assays) + + assert len(s2.RegisteredAssays) >= 0 # Just check it exists + assert any(a.Identifier == a3_id for a in arc.Assays) + + +@pytest.mark.asyncio +async def test_assay_with_complete_ontology_fields(workflow_tester: WorkflowTester) -> None: + """Test assay with all ontology-related fields filled (measurement, technology, platform).""" + inv_id = "INV_ONTOLOGY" + assay_id = "ASSAY_ONT" + + investigations = [{"identifier": inv_id, "title": "Ontology Test"}] + assays = [ { - "id": 10, - "investigation_id": 1, - "title": "S1", - "description": "D1", - "submission_time": None, - "release_time": None, + "identifier": assay_id, + "investigation_ref": inv_id, + "measurement_type_term": "gene expression profiling", + "measurement_type_uri": "http://purl.obolibrary.org/obo/OBI_0001271", + "measurement_type_version": "v1", + "technology_type_term": "nucleotide sequencing", + "technology_type_uri": "http://purl.obolibrary.org/obo/OBI_0000626", + "technology_type_version": "v1", + "technology_platform": "Illumina HiSeq 2500", + } + ] + + workflow_tester.set_db_content( + investigations=investigations, + assays=assays, + ) + + arcs = await workflow_tester.run() + + assert len(arcs) == 1 + arc = arcs[0] + assert len(arc.Assays) == 1 + assay = arc.Assays[0] + + # Verify Measurement Type + assert assay.MeasurementType is not None, "MeasurementType is None" + assert assay.MeasurementType.Name == "gene expression profiling" + # Match either full URI or CURIE + assert ( + assay.MeasurementType.TermAccessionNumber is not None and "0001271" in assay.MeasurementType.TermAccessionNumber + ) + + # Verify Technology Type + assert assay.TechnologyType is not None, "TechnologyType is None" + assert assay.TechnologyType.Name == "nucleotide sequencing" + assert ( + assay.TechnologyType.TermAccessionNumber is not None and "0000626" in assay.TechnologyType.TermAccessionNumber + ) + + # Verify Technology Platform + assert assay.TechnologyPlatform is not None, "TechnologyPlatform is None" + assert assay.TechnologyPlatform.Name == "Illumina HiSeq 2500" + + +@pytest.mark.asyncio +async def test_assay_with_annotations(workflow_tester: WorkflowTester) -> None: + """ + Test investigation with an assay and annotation table data. + + Note: This is 'Neuland' because the reconstruction of tables from the flat + database view is still a TODO in main.py. This test ensures the workflow + runs and demonstrates how the data structure looks. + """ + inv_id = "INV_ANN" + assay_id = "ASSAY_ANN" + + investigations = [{"identifier": inv_id, "title": "Annotation Test"}] + assays = [{"identifier": assay_id, "investigation_ref": inv_id}] + + # Example annotation rows representing a table + # These rows logically form a table 'Sample Metadata' with 2 rows and 2 columns + annotations = [ + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "table_name": "Sample Metadata", + "row_index": 0, + "column_name": "Source Name", + "value": "Sample 1", }, { - "id": 11, - "investigation_id": 2, - "title": "S2", - "description": "D2", - "submission_time": None, - "release_time": None, + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "table_name": "Sample Metadata", + "row_index": 0, + "column_name": "Characteristics [Species]", + "value": "Homo sapiens", + }, + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "table_name": "Sample Metadata", + "row_index": 1, + "column_name": "Source Name", + "value": "Sample 2", + }, + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "table_name": "Sample Metadata", + "row_index": 1, + "column_name": "Characteristics [Species]", + "value": "Mus musculus", }, ] - ass = [{"id": 100, "study_id": 10}, {"id": 101, "study_id": 11}] - # Configure cursor behavior using helper - mock_detail_cursor = _setup_cursor_side_effects(mock_db_cursor, invs, sts, ass) + workflow_tester.set_db_content( + investigations=investigations, + assays=assays, + annotations=annotations, + ) - await main() + # Currently, _process_annotation_tables in main.py is a placeholder. + # The test verifies that the pipeline handles the data gracefully. + arcs = await workflow_tester.run() - # Verify interactions - assert mock_db_connection.cursor.called - assert mock_db_cursor.execute.call_count == 1 - assert mock_detail_cursor.execute.call_count == 2 # noqa: PLR2004 - assert mock_api_client.create_or_update_arc.called + assert len(arcs) == 1 + arc = arcs[0] + assert arc.Identifier == inv_id + assert arc.Assays[0].Identifier == assay_id - all_arcs = [call.kwargs["arc"] for call in mock_api_client.create_or_update_arc.call_args_list] - assert len(all_arcs) == 2 # noqa: PLR2004 + # For now, we expect no tables to be created because of the placeholder. + # When implemented, TableCount should be 1. + assert arc.Assays[0].TableCount == 1 + assert arc.Assays[0].Tables[0].Name == "Sample Metadata" + assert arc.Assays[0].Tables[0].RowCount == 2 # noqa: PLR2004 + assert arc.Assays[0].Tables[0].ColumnCount == 2 # noqa: PLR2004 - # Verify content of uploaded ARCs (Identifiers from invs list) - identifiers = set() - for arc in all_arcs: - # Find the investigation node in @graph - investigation_node = next( - (node for node in arc.get("@graph", []) if "Investigation" in node.get("additionalType", "")), None - ) - if investigation_node: - identifiers.add(investigation_node.get("identifier")) - assert identifiers == {"1", "2"} +@pytest.mark.asyncio +async def test_comprehensive_annotation_flow(workflow_tester: WorkflowTester) -> None: # noqa: PLR0914 + """ + Test a complete flow with multiple linked annotation tables. + + Study: Sources -> Samples (with Characteristics and Factors) + Assay Table 1: Samples -> Extracts (with Parameters) + Assay Table 2: Extracts -> Data (with Parameters and Unitized Cells). + """ + inv_id = "INV_FLOW" + study_id = "STUDY_FLOW" + assay_id = "ASSAY_FLOW" + + investigations = [{"identifier": inv_id, "title": "Comprehensive Flow Test"}] + studies = [{"identifier": study_id, "investigation_ref": inv_id, "title": "Study Flow"}] + assays = [{"identifier": assay_id, "investigation_ref": inv_id, "study_ref": json.dumps([study_id])}] + + annotations = [ + # --- Study Table: "Samples" --- + { + "investigation_ref": inv_id, + "target_type": "study", + "target_ref": study_id, + "table_name": "Samples", + "row_index": 0, + "column_type": "input", + "column_io_type": "source_name", + "cell_value": "Source_A", + }, + { + "investigation_ref": inv_id, + "target_type": "study", + "target_ref": study_id, + "table_name": "Samples", + "row_index": 0, + "column_type": "characteristic", + "column_annotation_term": "Species", + "cell_annotation_term": "Arabidopsis thaliana", + "cell_annotation_uri": "http://purl.obolibrary.org/obo/NCBITaxon_3702", + }, + { + "investigation_ref": inv_id, + "target_type": "study", + "target_ref": study_id, + "table_name": "Samples", + "row_index": 0, + "column_type": "factor", + "column_annotation_term": "Treatment", + "cell_annotation_term": "Drought", + }, + { + "investigation_ref": inv_id, + "target_type": "study", + "target_ref": study_id, + "table_name": "Samples", + "row_index": 0, + "column_type": "output", + "column_io_type": "sample_name", + "cell_value": "Sample_1", + }, + # --- Assay Table 1: "Extraction" --- + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "table_name": "Extraction", + "row_index": 0, + "column_type": "input", + "column_io_type": "sample_name", + "cell_value": "Sample_1", + }, + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "table_name": "Extraction", + "row_index": 0, + "column_type": "parameter", + "column_annotation_term": "Method", + "cell_value": "Phenol-Chloroform", + }, + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "table_name": "Extraction", + "row_index": 0, + "column_type": "output", + "column_io_type": "sample_name", # ISA uses sample_name for extracts often + "cell_value": "Extract_1", + }, + # --- Assay Table 2: "Sequencing" --- + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "table_name": "Sequencing", + "row_index": 0, + "column_type": "input", + "column_io_type": "sample_name", + "cell_value": "Extract_1", + }, + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "table_name": "Sequencing", + "row_index": 0, + "column_type": "parameter", + "column_annotation_term": "Concentration", + "cell_value": "50.5", + "cell_annotation_term": "ng/ul", # Unitized cell + }, + { + "investigation_ref": inv_id, + "target_type": "assay", + "target_ref": assay_id, + "table_name": "Sequencing", + "row_index": 0, + "column_type": "output", + "column_io_type": "data", + "cell_value": "raw_data.fastq.gz", + }, + ] + + workflow_tester.set_db_content( + investigations=investigations, + studies=studies, + assays=assays, + annotations=annotations, + ) + + arcs = await workflow_tester.run() + arc = arcs[0] + + # Verify Study Table "Samples" + study = arc.Studies[0] + assert study.TableCount == 1 + sample_table = study.Tables[0] + assert sample_table.Name == "Samples" + assert sample_table.ColumnCount == 4 # noqa: PLR2004 + # Check Header types (order preserved by implementation) + assert sample_table.Headers[0].is_input + assert sample_table.Headers[1].is_characteristic + assert sample_table.Headers[2].is_factor + assert sample_table.Headers[3].is_output + + # Verify Assay Tables + assay = arc.Assays[0] + assert assay.TableCount == 2 # noqa: PLR2004 + + extraction_table = next(t for t in assay.Tables if t.Name == "Extraction") + assert extraction_table.ColumnCount == 3 # noqa: PLR2004 + assert extraction_table.Headers[1].is_parameter + + sequencing_table = next(t for t in assay.Tables if t.Name == "Sequencing") + assert sequencing_table.ColumnCount == 3 # noqa: PLR2004 + # Check unitized cell + conc_col_idx = 1 + cell = sequencing_table.GetCellAt(conc_col_idx, 0) + assert cell.is_unitized + assert cell.GetContent()[0] == "50.5" + assert cell.GetContent()[1] == "ng/ul" diff --git a/middleware/sql_to_arc/tests/unit/test_builder.py b/middleware/sql_to_arc/tests/unit/test_builder.py new file mode 100644 index 0000000..85b6f7b --- /dev/null +++ b/middleware/sql_to_arc/tests/unit/test_builder.py @@ -0,0 +1,199 @@ +"""Unit tests for the ARC builder module.""" + +import json +from typing import Any + +import pytest + +from middleware.sql_to_arc.builder import build_single_arc_task +from middleware.sql_to_arc.context import ArcBuildData +from middleware.sql_to_arc.models import ( + AssayRow, + ContactRow, + InvestigationRow, + PublicationRow, + StudyRow, +) + + +@pytest.fixture +def sample_investigation() -> dict[str, Any]: + """Return a sample investigation dictionary.""" + return { + "identifier": "inv1", + "title": "Inv Title", + "description_text": "Inv Desc", + "submission_date": None, + "public_release_date": None, + } + + +@pytest.fixture +def sample_studies() -> list[dict[str, Any]]: + """Return a list of sample study dictionaries.""" + return [ + { + "identifier": "sty1", + "investigation_ref": "inv1", + "title": "Study Title", + "description_text": "Study Desc", + "submission_date": None, + "public_release_date": None, + } + ] + + +@pytest.fixture +def sample_assays() -> list[dict[str, Any]]: + """Return a list of sample assay dictionaries.""" + return [ + { + "identifier": "asy1", + "investigation_ref": "inv1", + "measurement_type_term": "MType", + "measurement_type_uri": "http://mtype", + "technology_type_term": "TType", + "technology_type_uri": "http://ttype", + # Link to study sty1 + "study_ref": '["sty1"]', + "technology_platform": "Platform", + } + ] + + +@pytest.fixture +def sample_contacts() -> list[dict[str, Any]]: + """Return a list of sample contact dictionaries.""" + return [ + { + "last_name": "Doe", + "first_name": "John", + "investigation_ref": "inv1", + "target_type": "investigation", + "target_ref": None, + }, + { + "last_name": "Smith", + "first_name": "Jane", + "investigation_ref": "inv1", + "target_type": "study", + "target_ref": "sty1", + }, + ] + + +@pytest.fixture +def sample_publications() -> list[dict[str, Any]]: + """Return a list of sample publication dictionaries.""" + return [ + { + "title": "Inv Pub", + "investigation_ref": "inv1", + "target_type": "investigation", + "target_ref": None, + }, + { + "title": "Study Pub", + "investigation_ref": "inv1", + "target_type": "study", + "target_ref": "sty1", + }, + ] + + +def test_build_simple_arc(sample_investigation: dict[str, Any]) -> None: + """Test building a basic ARC structure from investigation data.""" + arc_data = ArcBuildData( + investigation_row=InvestigationRow.model_validate(sample_investigation), + studies=[], + assays=[], + contacts=[], + publications=[], + annotations=[], + ) + arc_json = build_single_arc_task(arc_data) + assert isinstance(arc_json, str) + + res = json.loads(arc_json) + # RO-Crate JSON-LD usually has a @graph + graph = res.get("@graph", []) + # Find the investigation (Dataset with identifier or specific type) + inv = next((item for item in graph if item.get("@id") == "inv1" or item.get("identifier") == "inv1"), None) + assert inv is not None + + +def test_build_arc_with_study_and_assay( + sample_investigation: dict[str, Any], sample_studies: list[dict[str, Any]], sample_assays: list[dict[str, Any]] +) -> None: + """Test building an ARC with nested study and assay structures.""" + arc_data = ArcBuildData( + investigation_row=InvestigationRow.model_validate(sample_investigation), + studies=[StudyRow.model_validate(s) for s in sample_studies], + assays=[AssayRow.model_validate(a) for a in sample_assays], + contacts=[], + publications=[], + annotations=[], + ) + arc_json = build_single_arc_task(arc_data) + res = json.loads(arc_json) + graph = res.get("@graph", []) + + # Check for study and assay in the graph + study = next((item for item in graph if item.get("@id") == "sty1" or item.get("identifier") == "sty1"), None) + assay = next((item for item in graph if item.get("@id") == "asy1" or item.get("identifier") == "asy1"), None) + + assert study is not None + assert assay is not None + + +def test_build_arc_with_contacts_and_pubs( + sample_investigation: dict[str, Any], + sample_studies: list[dict[str, Any]], + sample_contacts: list[dict[str, Any]], + sample_publications: list[dict[str, Any]], +) -> None: + """Test building an ARC with contacts and publications at both investigation and study levels.""" + arc_data = ArcBuildData( + investigation_row=InvestigationRow.model_validate(sample_investigation), + studies=[StudyRow.model_validate(s) for s in sample_studies], + assays=[], + contacts=[ContactRow.model_validate(c) for c in sample_contacts], + publications=[PublicationRow.model_validate(p) for p in sample_publications], + annotations=[], + ) + arc_json = build_single_arc_task(arc_data) + res = json.loads(arc_json) + graph = res.get("@graph", []) + + # Check for contacts (usually Person type) + doe = next((item for item in graph if item.get("familyName") == "Doe"), None) + smith = next((item for item in graph if item.get("familyName") == "Smith"), None) + + assert doe is not None + assert smith is not None + + +def test_build_ignores_irrelevant_data(sample_investigation: dict[str, Any]) -> None: + """Test that data linked to other investigations is correctly filtered out.""" + # Data for other investigation + other_study = { + "identifier": "styX", + "investigation_ref": "inv2", + "title": "Other Study", + } + + arc_data = ArcBuildData( + investigation_row=InvestigationRow.model_validate(sample_investigation), + studies=[StudyRow.model_validate(other_study)], + assays=[], + contacts=[], + publications=[], + annotations=[], + ) + arc_json = build_single_arc_task(arc_data) + res = json.loads(arc_json) + graph = res.get("@graph", []) + + # Check that styX is NOT in the graph + sty_x = next((item for item in graph if item.get("@id") == "styX" or item.get("identifier") == "styX"), None) + assert sty_x is None diff --git a/middleware/sql_to_arc/tests/unit/test_coverage.py b/middleware/sql_to_arc/tests/unit/test_coverage.py deleted file mode 100644 index c917421..0000000 --- a/middleware/sql_to_arc/tests/unit/test_coverage.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Additional tests to increase coverage for sql_to_arc/main.py.""" - -import asyncio -import json -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest - -from middleware.sql_to_arc.main import ( - DatasetContext, - ProcessingStats, - WorkerContext, - build_arc_for_investigation, - process_single_dataset, - stream_investigation_datasets, -) - - -def test_processing_stats_to_jsonld() -> None: - """Test ProcessingStats.to_jsonld conversion.""" - stats = ProcessingStats( - found_datasets=2, - total_studies=4, - total_assays=10, - failed_datasets=1, - failed_ids=["error-1"], - duration_seconds=12.5, - ) - jsonld_str = stats.to_jsonld(rdi_identifier="test-rdi", rdi_url="https://test-rdi.org") - data = json.loads(jsonld_str) - - assert data["@type"] == ["prov:Activity", "schema:CreateAction"] - assert data["found_datasets"] == 2 # noqa: PLR2004 - assert data["status"] == "schema:FailedActionStatus" - assert data["duration"] == "PT12.50S" - assert data["prov:used"]["schema:identifier"] == "test-rdi" - - -@pytest.mark.asyncio -async def test_stream_investigation_datasets() -> None: - """Test stream_investigation_datasets with mocked cursor.""" - mock_cur = AsyncMock() - mock_cur.fetchmany.side_effect = [ - [{"id": 1, "title": "Inv 1"}], - [], # End of stream - ] - - # Mock studies and assays - mock_detail_cur = AsyncMock() - mock_cursor_cm = MagicMock() - mock_cursor_cm.__aenter__.return_value = mock_detail_cur - mock_cursor_cm.__aexit__.return_value = False - - # mock_conn.cursor() should return the context manager - mock_conn = MagicMock() - mock_conn.cursor.return_value = mock_cursor_cm - mock_cur.connection = mock_conn - - # Detail fetches (Studies, then Assays) - mock_detail_cur.fetchall.side_effect = [ - [{"id": 10, "investigation_id": 1, "title": "Study 1"}], # Studies - [{"id": 100, "study_id": 10, "measurement_type": "MT"}], # Assays - ] - - results = [] - async for item in stream_investigation_datasets(mock_cur, batch_size=1): - results.append(item) - - assert len(results) == 1 - inv_row, studies, assays = results[0] - assert inv_row["id"] == 1 - assert len(studies) == 1 - assert studies[0]["id"] == 10 # noqa: PLR2004 - assert 10 in assays # noqa: PLR2004 - - -def test_build_arc_for_investigation() -> None: - """Test build_arc_for_investigation direct call.""" - inv_row = {"id": 1, "title": "Inv"} - studies = [{"id": 10, "title": "Study"}] - assays_by_study = {10: [{"id": 100, "measurement_type": "M"}]} - - with ( - patch("middleware.sql_to_arc.main.map_investigation") as mock_map_inv, - patch("middleware.sql_to_arc.main.map_study") as mock_map_study, - patch("middleware.sql_to_arc.main.map_assay") as mock_map_assay, - patch("arctrl.ARC.from_arc_investigation") as mock_arc_from_inv, - ): - mock_inv = MagicMock() - mock_map_inv.return_value = mock_inv - mock_study = MagicMock() - mock_map_study.return_value = mock_study - mock_assay = MagicMock() - mock_map_assay.return_value = mock_assay - - mock_arc = MagicMock() - mock_arc.ToROCrateJsonString.return_value = '{"fake": "arc"}' - mock_arc_from_inv.return_value = mock_arc - - result = build_arc_for_investigation(inv_row, studies, assays_by_study) - assert result == '{"fake": "arc"}' - - -@pytest.mark.asyncio -async def test_process_single_dataset_limits_exceeded() -> None: - """Test process_single_dataset when limits are exceeded.""" - ctx = WorkerContext( - client=AsyncMock(), - rdi="test", - executor=MagicMock(), - max_studies=1, - max_assays=1, - arc_generation_timeout_minutes=1, - ) - - # 2 studies exceeds limit of 1 - dataset_ctx = DatasetContext(investigation_row={"id": "err1"}, studies=[{"id": 1}, {"id": 2}], assays_by_study={}) - - stats = ProcessingStats() - semaphore = asyncio.Semaphore(1) - - await process_single_dataset(ctx, dataset_ctx, semaphore, stats) - assert stats.failed_datasets == 1 - assert "err1" in stats.failed_ids - - -@pytest.mark.asyncio -async def test_process_single_dataset_timeout() -> None: - """Test process_single_dataset when build times out.""" - ctx = WorkerContext( - client=AsyncMock(), - rdi="test", - executor=MagicMock(), - max_studies=10, - max_assays=10, - arc_generation_timeout_minutes=1, - ) - - dataset_ctx = DatasetContext(investigation_row={"id": "timeout1"}, studies=[], assays_by_study={}) - - stats = ProcessingStats() - semaphore = asyncio.Semaphore(1) - - with patch("asyncio.wait_for", side_effect=asyncio.TimeoutError): - await process_single_dataset(ctx, dataset_ctx, semaphore, stats) - - assert stats.failed_datasets == 1 - assert "timeout1" in stats.failed_ids diff --git a/middleware/sql_to_arc/tests/unit/test_database.py b/middleware/sql_to_arc/tests/unit/test_database.py new file mode 100644 index 0000000..dae04c8 --- /dev/null +++ b/middleware/sql_to_arc/tests/unit/test_database.py @@ -0,0 +1,154 @@ +"""Unit tests for the Database class in middleware.sql_to_arc.database. + +These tests cover async methods for retrieving investigations, studies, assays, +contacts, publications, and annotation tables using mocked database connections. +""" + +from collections.abc import AsyncIterable, Iterable +from typing import Any +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from middleware.sql_to_arc.database import Database +from middleware.sql_to_arc.stats import ProcessingStats + + +class AsyncIterator: + """Helper to mock an async iterator.""" + + def __init__(self, data: Iterable[Any]) -> None: + """Initialize the async iterator with the given data.""" + self.data = iter(data) + + def __aiter__(self) -> "AsyncIterator": + """Return the async iterator.""" + return self + + async def __anext__(self) -> Any: + """Return the next value in the iterator.""" + try: + return next(self.data) + except StopIteration as exc: + raise StopAsyncIteration from exc + + +async def collect_gen(gen: AsyncIterable[Any]) -> list[Any]: + """Collect async generator results.""" + return [row async for row in gen] + + +@pytest.mark.asyncio +async def test_stream_investigations() -> None: + """Test the stream_investigations method of the Database class.""" + with patch("middleware.sql_to_arc.database.create_async_engine") as mock_engine: + mock_conn = AsyncMock() + mock_engine.return_value.connect.return_value.__aenter__.return_value = mock_conn + + mock_result = AsyncMock() + # Ensure mappings() is a regular mock, not an AsyncMock, so it returns immediately + mock_result.mappings = MagicMock() + mock_result.mappings.return_value = AsyncIterator([ + {"identifier": "1", "title": "Test Investigation", "description_text": "Test Desc"} + ]) + mock_conn.stream.return_value = mock_result + + db = Database("sqlite+aiosqlite:///") + res = await collect_gen(db.stream_investigations(stats=ProcessingStats(), limit=5)) + + assert len(res) == 1 + assert res[0].identifier == "1" + assert res[0].title == "Test Investigation" + assert res[0].description_text == "Test Desc" + mock_conn.stream.assert_called() + + +@pytest.mark.asyncio +async def test_stream_studies() -> None: + """Test the stream_studies method of the Database class.""" + with patch("middleware.sql_to_arc.database.create_async_engine") as mock_engine: + mock_conn = AsyncMock() + mock_engine.return_value.connect.return_value.__aenter__.return_value = mock_conn + + mock_result = AsyncMock() + mock_result.mappings = MagicMock() + mock_result.mappings.return_value = AsyncIterator([ + {"identifier": "10", "investigation_ref": "1", "title": "Test Study"} + ]) + mock_conn.stream.return_value = mock_result + + db = Database("connection_string") + res = await collect_gen(db.stream_studies(["1", "2"])) + + assert len(res) == 1 + assert res[0].identifier == "10" + assert res[0].title == "Test Study" + mock_conn.stream.assert_called() + + +@pytest.mark.asyncio +async def test_stream_assays() -> None: + """Test the stream_assays method of the Database class.""" + with patch("middleware.sql_to_arc.database.create_async_engine") as mock_engine: + mock_conn = AsyncMock() + mock_engine.return_value.connect.return_value.__aenter__.return_value = mock_conn + + mock_result = AsyncMock() + mock_result.mappings = MagicMock() + mock_result.mappings.return_value = AsyncIterator([]) + mock_conn.stream.return_value = mock_result + + db = Database("connection_string") + await collect_gen(db.stream_assays(["1"])) + mock_conn.stream.assert_called() + + +@pytest.mark.asyncio +async def test_stream_contacts() -> None: + """Test the stream_contacts method of the Database class.""" + with patch("middleware.sql_to_arc.database.create_async_engine") as mock_engine: + mock_conn = AsyncMock() + mock_engine.return_value.connect.return_value.__aenter__.return_value = mock_conn + + mock_result = AsyncMock() + mock_result.mappings = MagicMock() + mock_result.mappings.return_value = AsyncIterator([]) + mock_conn.stream.return_value = mock_result + + db = Database("connection_string") + await collect_gen(db.stream_contacts(["1"])) + mock_conn.stream.assert_called() + + +@pytest.mark.asyncio +async def test_stream_publications() -> None: + """Test the stream_publications method of the Database class.""" + with patch("middleware.sql_to_arc.database.create_async_engine") as mock_engine: + mock_conn = AsyncMock() + mock_engine.return_value.connect.return_value.__aenter__.return_value = mock_conn + + mock_result = AsyncMock() + mock_result.mappings = MagicMock() + mock_result.mappings.return_value = AsyncIterator([]) + mock_conn.stream.return_value = mock_result + + db = Database("connection_string") + await collect_gen(db.stream_publications(["1"])) + mock_conn.stream.assert_called() + + +@pytest.mark.asyncio +async def test_stream_annotation_tables() -> None: + """Test the stream_annotation_tables method of the Database class.""" + with patch("middleware.sql_to_arc.database.create_async_engine") as mock_engine: + mock_conn = AsyncMock() + mock_engine.return_value.connect.return_value.__aenter__.return_value = mock_conn + + mock_result = AsyncMock() + mock_result.mappings = MagicMock() + mock_result.mappings.return_value = AsyncIterator([]) + mock_conn.stream.return_value = mock_result + + db = Database("connection_string") + await collect_gen(db.stream_annotation_tables(["1"])) + mock_conn.stream.assert_called() diff --git a/middleware/sql_to_arc/tests/unit/test_database_missing_tables.py b/middleware/sql_to_arc/tests/unit/test_database_missing_tables.py new file mode 100644 index 0000000..4e19d76 --- /dev/null +++ b/middleware/sql_to_arc/tests/unit/test_database_missing_tables.py @@ -0,0 +1,49 @@ +"""Tests for Database class handling of missing tables and views. + +This module tests error handling when database tables or views do not exist, +ensuring that ProgrammingError exceptions are properly caught and logged. +""" + +from unittest.mock import AsyncMock, patch + +import pytest +from sqlalchemy.exc import ProgrammingError + +from middleware.sql_to_arc.database import Database +from middleware.sql_to_arc.stats import ProcessingStats + + +@pytest.mark.asyncio +async def test_stream_investigations_missing_table(caplog: pytest.LogCaptureFixture) -> None: + """Test stream_investigations when the table is missing.""" + with patch("middleware.sql_to_arc.database.create_async_engine") as mock_engine: + mock_conn = AsyncMock() + mock_engine.return_value.connect.return_value.__aenter__.return_value = mock_conn + + # Simulate ProgrammingError for missing relation + error_msg = 'relation "vInvestigation" does not exist' + mock_conn.stream.side_effect = ProgrammingError("SELECT", {}, Exception(error_msg)) + + db = Database("postgresql://localhost/db") + results = [row async for row in db.stream_investigations(stats=ProcessingStats())] + + assert len(results) == 0 + assert 'Table or view "vInvestigation" does not exist' in caplog.text + + +@pytest.mark.asyncio +async def test_stream_annotation_tables_missing_table(caplog: pytest.LogCaptureFixture) -> None: + """Test stream_annotation_tables when the table is missing.""" + with patch("middleware.sql_to_arc.database.create_async_engine") as mock_engine: + mock_conn = AsyncMock() + mock_engine.return_value.connect.return_value.__aenter__.return_value = mock_conn + + # Simulate ProgrammingError for missing relation + error_msg = 'relation "vannotationtable" does not exist' + mock_conn.stream.side_effect = ProgrammingError("SELECT", {}, Exception(error_msg)) + + db = Database("postgresql://localhost/db") + results = [row async for row in db.stream_annotation_tables(["1"])] + + assert len(results) == 0 + assert 'Table or view "vAnnotationTable" does not exist' in caplog.text diff --git a/middleware/sql_to_arc/tests/unit/test_database_sql_fix.py b/middleware/sql_to_arc/tests/unit/test_database_sql_fix.py new file mode 100644 index 0000000..581857e --- /dev/null +++ b/middleware/sql_to_arc/tests/unit/test_database_sql_fix.py @@ -0,0 +1,83 @@ +"""Tests for database SQL fixes.""" + +from collections.abc import AsyncGenerator +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from middleware.sql_to_arc.database import Database +from middleware.sql_to_arc.models import StudyRow + + +@pytest.mark.asyncio +async def test_stream_by_investigation_selects_all_columns() -> None: + """Verify _stream_by_investigation uses literal_column('*') for correct column capture.""" + # This tests the SQLAlchemy SQL generation fix we applied + db = Database("postgresql://dummy") + # Mock the engine and connection + mock_engine = MagicMock() + mock_conn = AsyncMock() + db.engine = mock_engine + mock_engine.connect.return_value.__aenter__.return_value = mock_conn + + # Mock stream to return an empty async iterator + async def async_iter() -> AsyncGenerator[None, None]: + if False: + yield # Trick to make it an async generator + + mock_result = MagicMock() + mock_result.mappings.return_value = async_iter() + mock_conn.stream.return_value = mock_result + + # Call _stream_by_investigation + ids = ["INV001"] + # We consume it + async for _ in db._stream_by_investigation(StudyRow, ids, "study"): + pass + + # Inspect the call to conn.stream() + assert mock_conn.stream.called + stmt = mock_conn.stream.call_args[0][0] + + # Verify the statement selects * + # In SQLAlchemy 2.0, the statement object should show the column as * + # stmt.selected_columns contains the columns in the SELECT clause + # literal_column("*") is translated to textual "*" + + # Check if any column is literal "*" + columns = list(stmt.selected_columns) + column_names = [str(c) for c in columns] + assert "*" in column_names or '"*"' in column_names or any("*" in name for name in column_names) + + # Also check that it's from the correct table + assert StudyRow.__view_name__ in str(stmt) + + +@pytest.mark.asyncio +async def test_stream_investigations_selects_all_columns() -> None: + """Verify stream_investigations uses literal_column('*') correctly.""" + db = Database("postgresql://dummy") + mock_engine = MagicMock() + mock_conn = AsyncMock() + db.engine = mock_engine + mock_engine.connect.return_value.__aenter__.return_value = mock_conn + + async def async_iter() -> AsyncGenerator[None, None]: + if False: + yield + + mock_result = MagicMock() + mock_result.mappings.return_value = async_iter() + mock_conn.stream.return_value = mock_result + + mock_stats = MagicMock() + async for _ in db.stream_investigations(mock_stats): + pass + + assert mock_conn.stream.called + stmt = mock_conn.stream.call_args[0][0] + + columns = list(stmt.selected_columns) + column_names = [str(c) for c in columns] + assert "*" in column_names or '"*"' in column_names + assert "vInvestigation" in str(stmt) diff --git a/middleware/sql_to_arc/tests/unit/test_database_validation.py b/middleware/sql_to_arc/tests/unit/test_database_validation.py new file mode 100644 index 0000000..8b26c61 --- /dev/null +++ b/middleware/sql_to_arc/tests/unit/test_database_validation.py @@ -0,0 +1,116 @@ +"""Unit tests for database validation in the SQL-to-ARC converter.""" + +import logging +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from sqlalchemy.ext.asyncio import AsyncConnection + +from middleware.sql_to_arc.database import SchemaValidator +from middleware.sql_to_arc.models import ( + BaseRow, + MissingRequiredColumnsError, + RequiredColumnsNullError, + spec_field, +) + + +class ValidationTestRow(BaseRow): + """Test model for validation.""" + + __view_name__ = "vTest" + id: str = spec_field(required=True) + optional: str | None = spec_field(default=None) + overridable: str = spec_field(required=True, default="default", allow_spec_override=True) + + +@pytest.mark.asyncio +async def test_schema_validator_missing_required_column() -> None: + """Test that missing required columns raise MissingRequiredColumnsError.""" + engine = MagicMock() + conn = AsyncMock(spec=AsyncConnection) + + mock_inspect = MagicMock() + mock_inspect.get_columns.return_value = [{"name": "optional"}] + conn.run_sync.side_effect = lambda f: f(mock_inspect) + + with patch("middleware.sql_to_arc.database.inspect", return_value=mock_inspect): + validator = SchemaValidator(engine) + with pytest.raises(MissingRequiredColumnsError) as excinfo: + await validator._validate_model(conn, ValidationTestRow) + + assert "id" in str(excinfo.value) + + +@pytest.mark.asyncio +async def test_schema_validator_missing_optional_column(caplog: pytest.LogCaptureFixture) -> None: + """Test that missing optional columns log a warning.""" + engine = MagicMock() + conn = AsyncMock(spec=AsyncConnection) + + mock_inspect = MagicMock() + mock_inspect.get_columns.return_value = [{"name": "id"}, {"name": "overridable"}] + conn.run_sync.side_effect = lambda f: f(mock_inspect) + + # Mock NULL check query results + mock_result = MagicMock() + mock_result.scalar.return_value = 0 + conn.execute.return_value = mock_result + + with patch("middleware.sql_to_arc.database.inspect", return_value=mock_inspect): + validator = SchemaValidator(engine) + with caplog.at_level(logging.WARNING): + await validator._validate_model(conn, ValidationTestRow) + + assert "is missing optional columns: optional" in caplog.text + + +@pytest.mark.asyncio +async def test_schema_validator_required_null_raises() -> None: + """Test that NULL values in required columns raise RequiredColumnsNullError.""" + engine = MagicMock() + conn = AsyncMock(spec=AsyncConnection) + + mock_inspect = MagicMock() + mock_inspect.get_columns.return_value = [{"name": "id"}, {"name": "overridable"}, {"name": "optional"}] + conn.run_sync.side_effect = lambda f: f(mock_inspect) + + # Mock NULL check for 'id' to return 5 nulls + mock_result = MagicMock() + mock_result.scalar.return_value = 5 + conn.execute.return_value = mock_result + + with patch("middleware.sql_to_arc.database.inspect", return_value=mock_inspect): + validator = SchemaValidator(engine) + with pytest.raises(RequiredColumnsNullError) as excinfo: + await validator._check_null_values(conn, ValidationTestRow, {"id", "overridable", "optional"}) + + assert "id" in str(excinfo.value) + + +@pytest.mark.asyncio +async def test_schema_validator_override_null_warns(caplog: pytest.LogCaptureFixture) -> None: + """Test that NULL values in overridable columns log a warning.""" + engine = MagicMock() + conn = AsyncMock(spec=AsyncConnection) + + mock_inspect = MagicMock() + mock_inspect.get_columns.return_value = [{"name": "id"}, {"name": "overridable"}] + conn.run_sync.side_effect = lambda f: f(mock_inspect) + + # Mock NULL check: 0 for id, 3 for overridable + mock_result_zero = MagicMock() + mock_result_zero.scalar.return_value = 0 + + mock_result_three = MagicMock() + mock_result_three.scalar.return_value = 3 + + conn.execute.side_effect = [mock_result_zero, mock_result_three] + + with patch("middleware.sql_to_arc.database.inspect", return_value=mock_inspect): + validator = SchemaValidator(engine) + with caplog.at_level(logging.WARNING): + await validator._check_null_values(conn, ValidationTestRow, {"id", "overridable"}) + + assert 'Column "overridable" contains 3 NULL values' in caplog.text + assert "replaced by model defaults" in caplog.text diff --git a/middleware/sql_to_arc/tests/unit/test_main.py b/middleware/sql_to_arc/tests/unit/test_main.py index 4f1622d..8fb2d47 100644 --- a/middleware/sql_to_arc/tests/unit/test_main.py +++ b/middleware/sql_to_arc/tests/unit/test_main.py @@ -1,218 +1,140 @@ -"""Tests for sql_to_arc main module.""" +"""Unit tests for the sql_to_arc main module. + +This module contains tests for argument parsing, investigation processing, +and workflow logic in the sql_to_arc pipeline. +""" import asyncio +import concurrent.futures from collections.abc import AsyncGenerator -from importlib.metadata import PackageNotFoundError from pathlib import Path from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest -from middleware.sql_to_arc.main import ( - DatasetContext, - ProcessingStats, - WorkerContext, - main, - parse_args, +from middleware.api_client import ApiClient +from middleware.sql_to_arc.config import Config +from middleware.sql_to_arc.context import RelatedDataBatch, WorkerContext +from middleware.sql_to_arc.main import parse_args +from middleware.sql_to_arc.models import InvestigationRow +from middleware.sql_to_arc.processor import ( + process_investigation, process_investigations, - process_single_dataset, ) +from middleware.sql_to_arc.stats import ProcessingStats class TestParseArgs: """Test suite for parse_args function.""" - def test_parse_args_default(self) -> None: + @staticmethod + def test_parse_args_default() -> None: """Test parse_args with default config.""" with patch("sys.argv", ["prog"]): args = parse_args() assert args.config == Path("config.yaml") - def test_parse_args_custom_config(self) -> None: + @staticmethod + def test_parse_args_custom_config() -> None: """Test parse_args with custom config file.""" with patch("sys.argv", ["prog", "-c", "/path/to/config.yaml"]): args = parse_args() assert args.config == Path("/path/to/config.yaml") - def test_parse_args_long_form(self) -> None: - """Test parse_args with long form --config.""" - with patch("sys.argv", ["prog", "--config", "/custom/config.yaml"]): - args = parse_args() - assert args.config == Path("/custom/config.yaml") - - def test_parse_args_ignores_unknown_args(self) -> None: - """Test parse_args ignores pytest and other unknown arguments.""" - with patch("sys.argv", ["prog", "-c", "config.yaml", "--unknown"]): - args = parse_args() - assert args.config == Path("config.yaml") - - def test_parse_args_version(self) -> None: - """Test parse_args with version flag.""" - with patch("sys.argv", ["prog", "--version"]): - args = parse_args() - assert args.version is True - - -class TestMain: - """Test suite for main function.""" - - @pytest.mark.asyncio - async def test_main_version(self, capsys: pytest.CaptureFixture[str]) -> None: - """Test main function with version flag.""" - with ( - patch("sys.argv", ["prog", "--version"]), - patch("middleware.sql_to_arc.main.version", return_value="1.2.3"), - patch("sys.exit") as mock_exit, - ): - await main() - captured = capsys.readouterr() - assert "sql_to_arc version: 1.2.3" in captured.out - mock_exit.assert_called_once_with(0) - - @pytest.mark.asyncio - async def test_main_version_unknown(self, capsys: pytest.CaptureFixture[str]) -> None: - """Test main function with version flag when package not installed.""" - with ( - patch("sys.argv", ["prog", "--version"]), - patch("middleware.sql_to_arc.main.version", side_effect=PackageNotFoundError), - patch("sys.exit") as mock_exit, - ): - await main() - captured = capsys.readouterr() - assert "sql_to_arc version: unknown" in captured.out - mock_exit.assert_called_once_with(0) - - -# TestFetchAllInvestigations and other bulk fetchers removed as they are integrated into stream - - -# Bulk fetch classes removed - @pytest.mark.asyncio -async def test_process_single_dataset_success(monkeypatch: pytest.MonkeyPatch) -> None: - """Test successful single dataset processing.""" - mock_client = AsyncMock() - # Mock create_or_update_arc response - mock_arc_resp = MagicMock() - mock_arc_resp.status.value = "created" - mock_client.create_or_update_arc.return_value = MagicMock(arcs=[mock_arc_resp]) - - investigation = {"id": 1, "title": "Inv", "description": "Desc"} - studies_by_investigation: dict[int, list[dict[str, Any]]] = {1: [{"id": 10}]} - assays_by_study: dict[int, list[dict[str, Any]]] = {10: []} - - mock_executor = MagicMock() - - # Mock loop and executor behavior - loop_mock = MagicMock() - - # Only ONE call now: build_arc_for_investigation returns JSON string directly - future: asyncio.Future[str] = asyncio.Future() - future.set_result('{"id": "arc-1", "Identifier": "1"}') - - loop_mock.run_in_executor.return_value = future - - monkeypatch.setattr("asyncio.get_event_loop", lambda: loop_mock) - - ctx = WorkerContext( - client=mock_client, - rdi="test_rdi", - executor=mock_executor, - max_studies=5000, - max_assays=10000, - arc_generation_timeout_minutes=60, - ) +async def test_process_investigation_builds_and_uploads(monkeypatch: pytest.MonkeyPatch) -> None: + """process_investigation should build ARC via executor and upload it.""" + mock_client = AsyncMock(spec=ApiClient) + mock_client.create_or_update_arc.return_value = MagicMock(arcs=[MagicMock(id="1")]) + investigation = InvestigationRow(identifier="1", title="Inv", description_text="Desc") stats = ProcessingStats() - - dataset_ctx = DatasetContext( - investigation_row=investigation, - studies=studies_by_investigation[1], - assays_by_study=assays_by_study, - ) semaphore = asyncio.Semaphore(1) - await process_single_dataset(ctx, dataset_ctx, semaphore, stats) - assert mock_client.create_or_update_arc.called - # Check that parsed JSON was passed - call_kwargs = mock_client.create_or_update_arc.call_args.kwargs - assert call_kwargs["rdi"] == "test_rdi" - assert call_kwargs["arc"] == {"id": "arc-1", "Identifier": "1"} - assert stats.failed_datasets == 0 - - -@pytest.mark.asyncio -async def test_process_single_dataset_failure(monkeypatch: pytest.MonkeyPatch) -> None: - """Test single dataset processing failure.""" - mock_client = AsyncMock() - mock_executor = MagicMock() - - # Mock build failure (returns None) - loop_future: asyncio.Future[None] = asyncio.Future() - loop_future.set_result(None) + # Mock the loop.run_in_executor to return a JSON string + loop_future: asyncio.Future[str] = asyncio.Future() + loop_future.set_result('{"Identifier": "1"}') loop_mock = MagicMock() loop_mock.run_in_executor.return_value = loop_future - monkeypatch.setattr("asyncio.get_event_loop", lambda: loop_mock) + monkeypatch.setattr("asyncio.get_event_loop", MagicMock(return_value=loop_mock)) + + executor = MagicMock(spec=concurrent.futures.ProcessPoolExecutor) ctx = WorkerContext( client=mock_client, rdi="test_rdi", - executor=mock_executor, - max_studies=5000, - max_assays=10000, - arc_generation_timeout_minutes=60, + studies_by_inv={}, + assays_by_inv={}, + contacts_by_inv={}, + pubs_by_inv={}, + anns_by_inv={}, + worker_id=1, + total_workers=1, + executor=executor, + arc_generation_timeout_minutes=1, ) - semaphore = asyncio.Semaphore(1) - stats = ProcessingStats() - - investigation = {"id": 1} - dataset_ctx = DatasetContext(investigation_row=investigation, studies=[], assays_by_study={}) - await process_single_dataset(ctx, dataset_ctx, semaphore, stats) + await process_investigation(ctx, investigation, stats, "Inv 1", semaphore) - assert not mock_client.create_or_update_arc.called - assert stats.failed_datasets == 1 - assert "1" in stats.failed_ids + loop_mock.run_in_executor.assert_called_once() + mock_client.create_or_update_arc.assert_called_once() @pytest.mark.asyncio -async def test_process_investigations(monkeypatch: pytest.MonkeyPatch) -> None: - """Test full process_investigations flow.""" - mock_cursor = AsyncMock() - mock_client = AsyncMock() - mock_config = MagicMock( +async def test_process_investigations_flow(monkeypatch: pytest.MonkeyPatch) -> None: + """Test full process_investigations flow with batching and streaming.""" + mock_db = MagicMock() + + # Mock DB stream methods + async def mock_gen(**kwargs: Any) -> AsyncGenerator[Any, None]: + stats = kwargs.get("stats") + data = [ + InvestigationRow(identifier="1", title="T1", description_text="D1"), + InvestigationRow(identifier="2", title="T2", description_text="D2"), + ] + if stats: + stats.found_datasets += len(data) + for item in data: + yield item + + mock_db.stream_investigations.side_effect = mock_gen + + # Mock related data fetch + async def mock_fetch_related(*_args: Any, **_kwargs: Any) -> RelatedDataBatch: + return RelatedDataBatch( + studies_by_inv={}, + assays_by_inv={}, + contacts_by_inv={}, + pubs_by_inv={}, + anns_by_inv={}, + study_count=1, + assay_count=0, + ) + + monkeypatch.setattr("middleware.sql_to_arc.processor._fetch_and_group_related_data", mock_fetch_related) + + mock_client = AsyncMock(spec=ApiClient) + mock_config = MagicMock(spec=Config) + mock_config.configure_mock( max_concurrent_arc_builds=2, max_concurrent_tasks=4, + db_batch_size=10, rdi="test", - db_batch_size=100, - max_studies=5000, - max_assays=10000, - arc_generation_timeout_minutes=60, + debug_limit=10, + arc_generation_timeout_minutes=30, ) - # Mock stream_investigation_datasets - async def mock_stream(*_args: Any, **_kwargs: Any) -> AsyncGenerator[tuple[dict, list, dict], None]: - yield ({"id": 1}, [{"id": 10}], {10: []}) - yield ({"id": 2}, [], {}) - yield ({"id": 3}, [], {}) - - monkeypatch.setattr("middleware.sql_to_arc.main.stream_investigation_datasets", mock_stream) - - # Mock process_single_dataset to avoid checking the whole flow details here - async def mock_process_single( - _ctx: WorkerContext, - _dataset_ctx: DatasetContext, - _sem: asyncio.Semaphore, - _stats: ProcessingStats, - ) -> None: - # Simulate success - return + # Mock process_investigation to simplify + async def mock_process_inv(*args: Any, **kwargs: Any) -> None: + pass - monkeypatch.setattr("middleware.sql_to_arc.main.process_single_dataset", mock_process_single) + monkeypatch.setattr("middleware.sql_to_arc.processor.process_investigation", mock_process_inv) - stats = await process_investigations(mock_cursor, mock_client, mock_config) + stats = await process_investigations(mock_db, mock_client, mock_config) - assert stats.found_datasets == 3 # noqa: PLR2004 + assert stats.found_datasets == 2 # noqa: PLR2004 + assert stats.total_studies == 1 + mock_db.stream_investigations.assert_called_with(stats=stats, limit=10) diff --git a/middleware/sql_to_arc/tests/unit/test_mapper.py b/middleware/sql_to_arc/tests/unit/test_mapper.py index 4a4498e..e8aab7a 100644 --- a/middleware/sql_to_arc/tests/unit/test_mapper.py +++ b/middleware/sql_to_arc/tests/unit/test_mapper.py @@ -1,23 +1,44 @@ """Unit tests for the mapper module.""" import datetime -from typing import Any -from arctrl import ArcAssay, ArcInvestigation, ArcStudy, Person, Publication # type: ignore[import-untyped] - -from middleware.sql_to_arc.mapper import map_assay, map_contact, map_investigation, map_publication, map_study +import pytest +from arctrl import ( + ArcAssay, + ArcInvestigation, + ArcStudy, + Person, + Publication, +) +from pydantic import ValidationError + +from middleware.sql_to_arc.mapper import ( + map_annotation, + map_assay, + map_contact, + map_investigation, + map_publication, + map_study, +) +from middleware.sql_to_arc.models import ( + AssayRow, + ContactRow, + InvestigationRow, + PublicationRow, + StudyRow, +) def test_map_investigation() -> None: """Test mapping of investigation data.""" now = datetime.datetime.now() - row: dict[str, Any] = { - "id": 123, - "title": "Test Investigation", - "description": "Test Description", - "submission_time": now, - "release_time": now, - } + row = InvestigationRow( + identifier="123", + title="Test Investigation", + description_text="Test Description", + submission_date=now, + public_release_date=now, + ) arc = map_investigation(row) @@ -31,15 +52,17 @@ def test_map_investigation() -> None: def test_map_investigation_defaults() -> None: """Test mapping of investigation data with missing optional fields.""" - row: dict[str, Any] = { - "id": 456, - } + row = InvestigationRow( + identifier="456", + title="Default Title", + description_text="Default Description", + ) arc = map_investigation(row) assert arc.Identifier == "456" - assert arc.Title == "" - assert arc.Description == "" + assert arc.Title == "Default Title" + assert arc.Description == "Default Description" assert arc.SubmissionDate is None assert arc.PublicReleaseDate is None @@ -47,13 +70,14 @@ def test_map_investigation_defaults() -> None: def test_map_study() -> None: """Test mapping of study data.""" now = datetime.datetime.now() - row: dict[str, Any] = { - "id": 1, - "title": "Test Study", - "description": "Study Description", - "submission_time": now, - "release_time": now, - } + row = StudyRow( + identifier="1", + investigation_ref="inv1", + title="Test Study", + description_text="Study Description", + submission_date=now, + public_release_date=now, + ) study = map_study(row) @@ -65,58 +89,117 @@ def test_map_study() -> None: assert study.PublicReleaseDate == now.isoformat() +def test_map_investigation_string_dates() -> None: + """Test mapping of investigation data with string dates.""" + row = InvestigationRow( + identifier="789", + title="Title", + description_text="Description", + submission_date=datetime.datetime.strptime("2023-01-01", "%Y-%m-%d"), + public_release_date=datetime.datetime.strptime("2023-12-31", "%Y-%m-%d"), + ) + arc = map_investigation(row) + assert arc.SubmissionDate == "2023-01-01T00:00:00" + assert arc.PublicReleaseDate == "2023-12-31T00:00:00" + + def test_map_assay() -> None: """Test mapping of assay data.""" - row: dict[str, Any] = { - "id": 1, - "measurement_type": "Proteomics", - "technology_type": "Mass Spectrometry", - } + row = AssayRow( + identifier="1", + study_ref='["sty1"]', # type: ignore[arg-type] + investigation_ref="inv1", + measurement_type_term="Proteomics", + measurement_type_uri="http://example.org/prot", + technology_type_term="Mass Spectrometry", + technology_type_uri="http://example.org/ms", + ) assay = map_assay(row) assert isinstance(assay, ArcAssay) assert assay.Identifier == "1" - # Note: measurement_type and technology_type are not set yet - # as they require proper OntologyTerm objects from the database + # Check OntologyAnnotations + assert assay.MeasurementType is not None + assert assay.MeasurementType.Name == "Proteomics" + assert assay.MeasurementType.TermAccessionNumber == "http://example.org/prot" + assert assay.TechnologyType is not None + assert assay.TechnologyType.Name == "Mass Spectrometry" + assert assay.TechnologyType.TermAccessionNumber == "http://example.org/ms" + + +def test_map_assay_with_platform() -> None: + """Test mapping of assay data including technology platform.""" + row = AssayRow( + identifier="2", + study_ref='["sty1"]', # type: ignore[arg-type] + investigation_ref="inv1", + technology_platform="Orbitrap", + ) + assay = map_assay(row) + assert assay.Identifier == "2" + assert assay.TechnologyPlatform is not None + assert assay.TechnologyPlatform.Name == "Orbitrap" + + +def test_map_publication() -> None: + """Test mapping of publication data.""" + row = PublicationRow( + investigation_ref="inv1", + target_type="investigation", + pubmed_id="12345", + doi="10.1234/5678", + authors="Doe J, Smith A", + title="A Great Paper", + status_term="Published", + ) + + pub = map_publication(row) + + assert isinstance(pub, Publication) + assert pub.PubMedID == "12345" + assert pub.DOI == "10.1234/5678" + assert pub.Authors == "Doe J, Smith A" + assert pub.Title == "A Great Paper" + assert pub.Status is not None + assert pub.Status.Name == "Published" def test_map_contact() -> None: """Test mapping of contact data.""" - row: dict[str, Any] = { - "first_name": "John", - "last_name": "Doe", - "email": "john@example.com", - "affiliation": "University of Research", - "roles": ( - '[{"term": "Principal Investigator", "uri": "http://purl.obolibrary.org/obo/MS_1001271", "version": "1.0"}]' - ), - } + row = ContactRow( + investigation_ref="inv1", + target_type="investigation", + last_name="Doe", + first_name="John", + email="john@example.com", + roles='[{"term": "Principal Investigator", "uri": "http://roles", "version": "1.0"}]', # type: ignore[arg-type] + ) person = map_contact(row) assert isinstance(person, Person) - assert person.FirstName == "John" assert person.LastName == "Doe" + assert person.FirstName == "John" assert person.EMail == "john@example.com" - assert person.Affiliation == "University of Research" assert len(person.Roles) == 1 + assert person.Roles[0] is not None assert person.Roles[0].Name == "Principal Investigator" + assert person.Roles[0].TermAccessionNumber == "http://roles" -def test_map_publication() -> None: - """Test mapping of publication data.""" - row: dict[str, Any] = { - "pub_med_id": "12345678", - "doi": "10.1000/123", - "authors": "Author A, Author B", - "title": "Title of Publication", - } +def test_map_contact_invalid_roles() -> None: + """Test mapping of contact data with invalid roles JSON.""" + with pytest.raises(ValidationError): + ContactRow( + investigation_ref="inv1", + target_type="investigation", + last_name="Smith", + roles="{invalid-json}", # type: ignore[arg-type] + ) - pub = map_publication(row) - assert isinstance(pub, Publication) - assert pub.PubMedID == "12345678" - assert pub.DOI == "10.1000/123" - assert pub.Authors == "Author A, Author B" - assert pub.Title == "Title of Publication" +def test_map_annotation() -> None: + """Test the map_annotation helper function.""" + row = {"data": "test_value"} + assert map_annotation(row) == row diff --git a/middleware/sql_to_arc/tests/unit/test_populate.py b/middleware/sql_to_arc/tests/unit/test_populate.py deleted file mode 100644 index 2b4d341..0000000 --- a/middleware/sql_to_arc/tests/unit/test_populate.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Unit tests for investigation population helpers.""" - -from typing import Any - -from middleware.sql_to_arc.main import build_single_arc_task - - -def _sample_investigation() -> dict[str, Any]: - return { - "id": 1, - "title": "Test Investigation", - "description": "Desc", - "submission_time": None, - "release_time": None, - } - - -def _sample_studies() -> list[dict[str, Any]]: - return [ - { - "id": 10, - "investigation_id": 1, - "title": "Study 1", - "description": "Desc 1", - "submission_time": None, - "release_time": None, - }, - { - "id": 11, - "investigation_id": 1, - "title": "Study 2", - "description": "Desc 2", - "submission_time": None, - "release_time": None, - }, - ] - - -def test_build_single_arc_task_populates_studies_and_assays() -> None: - """The helper should build an investigation with studies and assays.""" - assays_by_study = { - 10: [ - {"id": 100, "study_id": 10, "measurement_type": "Metabolomics", "technology_type": "MS"}, - {"id": 101, "study_id": 10, "measurement_type": "Proteomics", "technology_type": "MS"}, - ], - 11: [ - {"id": 102, "study_id": 11, "measurement_type": "Genomics", "technology_type": "Sequencing"}, - ], - } - - arc = build_single_arc_task( - _sample_investigation(), - _sample_studies(), - assays_by_study, - ) - - assert arc.Identifier == "1" - assert len(arc.RegisteredStudies) == 2 # noqa: PLR2004 - study1 = next(study for study in arc.RegisteredStudies if study.Identifier == "10") - study2 = next(study for study in arc.RegisteredStudies if study.Identifier == "11") - - assert len(study1.RegisteredAssays) == 2 # noqa: PLR2004 - assert len(study2.RegisteredAssays) == 1 - - -def test_build_single_arc_task_handles_empty_assays() -> None: - """The helper should handle studies without assays.""" - arc = build_single_arc_task( - _sample_investigation(), - _sample_studies(), - {}, - ) - - assert len(arc.RegisteredStudies) == 2 # noqa: PLR2004 - for study in arc.RegisteredStudies: - assert len(study.RegisteredAssays) == 0 diff --git a/middleware/sql_to_arc/tests/unit/test_sql_to_arc_config.py b/middleware/sql_to_arc/tests/unit/test_sql_to_arc_config.py index 06738aa..7355493 100644 --- a/middleware/sql_to_arc/tests/unit/test_sql_to_arc_config.py +++ b/middleware/sql_to_arc/tests/unit/test_sql_to_arc_config.py @@ -19,51 +19,20 @@ def test_config_creation() -> None: ) config = Config( - db_name="test_db", - db_user="test_user", - db_password=SecretStr("test_password"), - db_host="localhost", - db_port=5432, + connection_string=SecretStr("postgresql+asyncpg://user:pass@localhost:5432/db"), + debug_limit=5, rdi="edaphobase", rdi_url="https://edaphobase.org", api_client=api_client_config, log_level="INFO", - max_concurrent_tasks=10, otel=OtelConfig(), ) - assert config.db_name == "test_db" - assert config.db_user == "test_user" - assert config.db_password.get_secret_value() == "test_password" - assert config.db_host == "localhost" - assert config.db_port == 5432 # noqa: PLR2004 + assert config.connection_string.get_secret_value() == "postgresql+asyncpg://user:pass@localhost:5432/db" + assert config.debug_limit == 5 # noqa: PLR2004 assert config.rdi == "edaphobase" assert config.rdi_url == "https://edaphobase.org" assert config.log_level == "INFO" - # Default is 2x max_concurrent_arc_builds (5 * 2 = 10) - assert config.max_concurrent_tasks == 10 # noqa: PLR2004 - - -def test_config_max_concurrent_tasks_custom() -> None: - """Test creating a Config with custom max_concurrent_tasks.""" - api_client_config = ApiClientConfig( - api_url="https://api.example.com", - otel=OtelConfig(), - ) - config = Config( - db_name="test_db", - db_user="test_user", - db_password=SecretStr("test_password"), - db_host="localhost", - rdi="edaphobase", - rdi_url="https://edaphobase.org", - api_client=api_client_config, - max_concurrent_arc_builds=8, - max_concurrent_tasks=32, - otel=OtelConfig(), - ) - assert config.max_concurrent_arc_builds == 8 # noqa: PLR2004 - assert config.max_concurrent_tasks == 32 # noqa: PLR2004 def test_config_with_defaults() -> None: @@ -76,18 +45,12 @@ def test_config_with_defaults() -> None: ) config = Config( - db_name="test_db", - db_user="test_user", - db_password=SecretStr("secret"), - db_host="localhost", + connection_string=SecretStr("sqlite:///:memory:"), rdi="edaphobase", rdi_url="https://edaphobase.org", api_client=api_client_config, - max_concurrent_tasks=20, otel=OtelConfig(), ) # Check defaults - assert config.db_port == 5432 # Default port # noqa: PLR2004 - # Default is 4x max_concurrent_arc_builds (5 * 4 = 20) - assert config.max_concurrent_tasks == 20 # noqa: PLR2004 + assert config.debug_limit is None diff --git a/middleware/sql_to_arc/tests/unit/test_stats.py b/middleware/sql_to_arc/tests/unit/test_stats.py new file mode 100644 index 0000000..b62b117 --- /dev/null +++ b/middleware/sql_to_arc/tests/unit/test_stats.py @@ -0,0 +1,24 @@ +"""Unit tests for ProcessingStats in sql_to_arc. + +This module tests JSON-LD serialization and merging of ProcessingStats objects. +""" + +from middleware.sql_to_arc.stats import ProcessingStats + + +def test_processing_stats_jsonld() -> None: + """Test JSON-LD serialization and merging of ProcessingStats objects.""" + stats = ProcessingStats( + found_datasets=10, total_studies=5, total_assays=5, failed_datasets=1, failed_ids=["inv1"], duration_seconds=1.5 + ) + json_ld = stats.to_jsonld() + assert "schema:CreateAction" in json_ld + assert "PT1.50S" in json_ld + assert "inv1" in json_ld + + # Test merge + stats2 = ProcessingStats(found_datasets=5, failed_datasets=1, failed_ids=["inv2"]) + stats.merge(stats2) + assert stats.found_datasets == 15 # noqa: PLR2004 + assert stats.failed_datasets == 2 # noqa: PLR2004 + assert "inv2" in stats.failed_ids diff --git a/middleware/sql_to_arc/tests/unit/test_validation_fixes.py b/middleware/sql_to_arc/tests/unit/test_validation_fixes.py new file mode 100644 index 0000000..5e17d98 --- /dev/null +++ b/middleware/sql_to_arc/tests/unit/test_validation_fixes.py @@ -0,0 +1,87 @@ +"""Tests for validation fixes and spec overrides.""" + +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +import pytest +from pydantic import ValidationError + +import middleware.sql_to_arc.database +from middleware.sql_to_arc.database import SchemaValidator +from middleware.sql_to_arc.models import InvestigationRow, RequiredColumnsNullError, spec_field + + +def test_investigation_row_spec_override() -> None: + """Test that allow_spec_override correctly replaces None with default values.""" + # description_text has allow_spec_override=True and default="" + data = { + "identifier": "INV001", + "title": "Test Investigation", + "description_text": None, # SQL NULL + } + + # Should not raise ValidationError + row = InvestigationRow.model_validate(data) + + assert row.description_text == "" + assert row.identifier == "INV001" + + +def test_investigation_row_no_override_fails() -> None: + """Test that fields without allow_spec_override still fail on None.""" + # identifier does NOT have allow_spec_override=True + data = {"identifier": None, "title": "Test Investigation", "description_text": "Some description"} + + with pytest.raises(ValidationError) as excinfo: + InvestigationRow.model_validate(data) + + assert "identifier" in str(excinfo.value) + + +@pytest.mark.asyncio +async def test_schema_validator_warnings_on_null_with_override(mocker: Any) -> None: + """Test that SchemaValidator issues a warning when required fields contain NULL but allow override.""" + mock_engine = MagicMock() + mock_conn = AsyncMock() + + # Simple mock that returns 5 for everything + mock_result = MagicMock() + mock_result.scalar.return_value = 5 + mock_conn.execute.return_value = mock_result + + validator = SchemaValidator(mock_engine) + + # We'll use a field that is definitely required but allowed to override + # Let's mock a field in investigation that has spec_required=True + + class MockRow(InvestigationRow): + required_with_override: str = spec_field(required=True, allow_spec_override=True, default="override") + + db_columns = {"required_with_override"} + + mocker.patch.object(middleware.sql_to_arc.database.logger, "warning", side_effect=RuntimeError("Warning reached")) + + with pytest.raises(RuntimeError, match="Warning reached"): + await validator._check_null_values(mock_conn, MockRow, db_columns) + + +@pytest.mark.asyncio +async def test_schema_validator_error_on_null_without_override() -> None: + """Test that SchemaValidator raises an error when required fields contain NULL and no override is allowed.""" + mock_engine = MagicMock() + mock_conn = AsyncMock() + mock_result = MagicMock() + mock_result.scalar.return_value = 1 + mock_conn.execute.return_value = mock_result + + validator = SchemaValidator(mock_engine) + db_columns = {"identifier", "title", "description_text"} + + # identifier is required and has no override + # Note: _check_null_values iterates over model fields. + # We want to ensure it raises RequiredColumnsNullError when it hits identifier. + + with pytest.raises(RequiredColumnsNullError) as excinfo: + await validator._check_null_values(mock_conn, InvestigationRow, db_columns) + + assert "identifier" in str(excinfo.value) diff --git a/pyproject.toml b/pyproject.toml index 1ba2413..2f9208f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,9 +63,14 @@ lint.select = [ "SIM", # flake8-simplify "A", # flake8-builtins "ARG", # flake8-unused-arguments + "BLE", # flake8-blind-except (fängt zu generische Exceptions ab) "PLR", # Pylint Refactor (inkl. complexity rules wie too-many-return-statements) + "SLF", # flake8-self (Private member access) ] +# Muss für PLR0917 aktiviert sein +preview = true + # Maximale Zeilenlänge wie in pylint (Standard: 100) line-length = 120 @@ -86,17 +91,30 @@ known-first-party = ["middleware"] force-single-line = false combine-as-imports = true +[tool.ruff.lint.per-file-ignores] +"middleware/*/tests/**/*.py" = ["SLF001"] + # Black and isort replaced by ruff format - configurations removed to avoid conflicts [tool.mypy] python_version = "3.12" +mypy_path = "middleware/sql_to_arc/src" +namespace_packages = true +explicit_package_bases = true +strict = true +plugins = ["pydantic.mypy"] + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true + warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true disallow_incomplete_defs = true explicit_package_bases = true namespace_packages = true -mypy_path = "middleware/sql_to_arc/src" # Exclude directories from type checking exclude = [ @@ -111,6 +129,18 @@ exclude = [ ".ruff_cache", ] +# [[tool.mypy.overrides]] +# module = [ +# "middleware.api_client.*", +# "middleware.shared.*", +# ] +# ignore_missing_imports = true + +# arctrl has no type stubs and no py.typed marker — suppress the resulting noise +[[tool.mypy.overrides]] +module = ["arctrl"] +ignore_missing_imports = true + # Docstring-Regeln aktivieren, damit es wie pylint C0114/15/16 meckert [tool.ruff.lint.pydocstyle] convention = "pep257" # oder "numpy", oder "google" @@ -142,6 +172,7 @@ markers = [ "asyncio: marks tests as async", "unit: marks tests as unit tests", "integration: marks tests as integration tests", + "system: marks tests as system tests", ] [tool.coverage.run] @@ -245,8 +276,17 @@ disable = [ # Additional opinionated/style checks often handled by formatters "R0903", # too-few-public-methods (opinionated, often not relevant) "R0913", # too-many-arguments (opinionated, case-by-case basis) + "R0917", # too-many-positional-arguments (handled by ruff PLR0917) "C0301", # line-too-long (handled by ruff formatter) "R0801", # duplicate-code (can be noisy in tests) + "R0914", # too-many-locals (handled by ruff PLR0914) + + # Exception checks (covered by ruff BLE001) + "W0718", # broad-exception-caught + "W0703", # broad-except + + # Private member access + "W0212", # protected-access ] [tool.hatch.version] diff --git a/scripts/load-env.sh b/scripts/load-env.sh index 3061864..829823e 100755 --- a/scripts/load-env.sh +++ b/scripts/load-env.sh @@ -83,6 +83,14 @@ else echo "⚠️ pre-commit not available - skipping hook installation" fi +# Sync python dependencies with uv +if command -v uv &> /dev/null; then + echo "🔧 Syncing Python dependencies..." + (cd "${mydir}/.." && uv sync --dev --all-packages) +else + echo "⚠️ uv not available - skipping dependency sync" +fi + ENCRYPTED_FILE="${mydir}/../.env.integration.enc" DECRYPTED_FILE="${mydir}/../.env" diff --git a/uv.lock b/uv.lock index 6137264..deabd03 100644 --- a/uv.lock +++ b/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.13'", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", "python_full_version < '3.13'", ] @@ -12,6 +13,30 @@ members = [ "sql-to-arc", ] +[[package]] +name = "aiomysql" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymysql" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/e0/302aeffe8d90853556f47f3106b89c16cc2ec2a4d269bdfd82e3f4ae12cc/aiomysql-0.3.2.tar.gz", hash = "sha256:72d15ef5cfc34c03468eb41e1b90adb9fd9347b0b589114bd23ead569a02ac1a", size = 108311, upload-time = "2025-10-22T00:15:21.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/af/aae0153c3e28712adaf462328f6c7a3c196a1c1c27b491de4377dd3e6b52/aiomysql-0.3.2-py3-none-any.whl", hash = "sha256:c82c5ba04137d7afd5c693a258bea8ead2aad77101668044143a991e04632eb2", size = 71834, upload-time = "2025-10-22T00:15:15.905Z" }, +] + +[[package]] +name = "aioodbc" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyodbc" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/87/3a7580938f217212a574ba0d1af78203fc278fc439815f3fc515a7fdc12b/aioodbc-0.5.0.tar.gz", hash = "sha256:cbccd89ce595c033a49c9e6b4b55bbace7613a104b8a46e3d4c58c4bc4f25075", size = 41298, upload-time = "2023-10-28T21:37:29.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/80/4d1565bc16b53cd603c73dc4bc770e2e6418d957417e05031314760dc28c/aioodbc-0.5.0-py3-none-any.whl", hash = "sha256:bcaf16f007855fa4bf0ce6754b1f72c6c5a3d544188849577ddd55c5dc42985e", size = 19449, upload-time = "2023-10-28T21:37:28.51Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -23,21 +48,21 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.1" +version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] [[package]] name = "api-client" -version = "8.3.2" -source = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fapi_client&branch=main#7d6c7b7b7a0cbe90b6ae68bc9c26feb298e8e0e4" } +version = "2.6.2.dev12+g4a93a5d9c" +source = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fapi_client&branch=main#4a93a5d9c140de1e2dbc5f3be6901943b0baaea7" } dependencies = [ { name = "httpx" }, { name = "pydantic" }, @@ -45,15 +70,15 @@ dependencies = [ [[package]] name = "arctrl" -version = "3.0.0b16" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openpyxl" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7a/d8/ce6b5642a6cbebd7c7cfe1b4818f0695108bc37f78b62c190ff2fcccf689/arctrl-3.0.0b16.tar.gz", hash = "sha256:a4ea987933ad78be485934c46bcfaa47e329085c6bd13bd0d362c6d9bc9a399d", size = 569631, upload-time = "2026-01-15T13:10:42.315Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/0b/bab2b576512c12de5ba1c4761be49bc88da1820c6969ce8c394ad0fe8a9f/arctrl-3.0.3.tar.gz", hash = "sha256:459c1818f4750f36b0535bf5740e615f8d0f969ec32a87a1a86a588707f9e92c", size = 656441, upload-time = "2026-03-04T08:52:18.225Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/f5/c099065f9139d60c07c41cdb138c2c2c00906c2ff35334fa8ff9dd7cbcda/arctrl-3.0.0b16-py3-none-any.whl", hash = "sha256:9ecc032e7ccad75487c2dbf3208dad8accfc6064e1b56c5f8e013e9eb61fac78", size = 851229, upload-time = "2026-01-15T13:10:40.672Z" }, + { url = "https://files.pythonhosted.org/packages/85/2a/eea9319c5f2622e1893a0ce993dc8fd296d3a7cddfc984b613c1fe43c553/arctrl-3.0.3-py3-none-any.whl", hash = "sha256:083c2a4b723e3a4d68d45b05b3645acdfb510168322f50d2b9d3fab4446d46cc", size = 952710, upload-time = "2026-03-04T08:52:16.549Z" }, ] [[package]] @@ -82,11 +107,68 @@ wheels = [ [[package]] name = "certifi" -version = "2026.1.4" +version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] @@ -100,59 +182,75 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] [[package]] @@ -166,86 +264,139 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, - { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, - { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, - { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, - { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, - { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, - { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, - { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, - { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, - { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, - { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, - { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, - { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, - { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, - { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, - { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, - { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, - { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, - { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, - { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, - { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, - { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, - { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, - { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, - { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, - { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, - { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, - { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, - { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, - { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, - { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, - { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, - { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, - { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, + { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, + { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, + { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, + { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, + { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, + { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, + { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, + { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, + { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, + { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, + { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, ] [[package]] @@ -277,64 +428,107 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.3" +version = "3.25.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] [[package]] name = "googleapis-common-protos" -version = "1.72.0" +version = "1.73.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/c0/4a54c386282c13449eca8bbe2ddb518181dc113e78d240458a68856b4d69/googleapis_common_protos-1.73.1.tar.gz", hash = "sha256:13114f0e9d2391756a0194c3a8131974ed7bffb06086569ba193364af59163b6", size = 147506, upload-time = "2026-03-26T22:17:38.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, + { url = "https://files.pythonhosted.org/packages/dc/82/fcb6520612bec0c39b973a6c0954b6a0d948aadfe8f7e9487f60ceb8bfa6/googleapis_common_protos-1.73.1-py3-none-any.whl", hash = "sha256:e51f09eb0a43a8602f5a915870972e6b4a394088415c79d79605a46d8e826ee8", size = 297556, upload-time = "2026-03-26T22:15:58.455Z" }, +] + +[[package]] +name = "greenlet" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] [[package]] name = "grpcio" -version = "1.78.0" +version = "1.80.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, - { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, - { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, - { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, - { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, - { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, - { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, - { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, - { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, - { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, - { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, - { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, - { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, - { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, - { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, - { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, - { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, - { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, - { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, + { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, + { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, + { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, + { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, + { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, + { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, + { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, + { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, ] [[package]] @@ -376,11 +570,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.16" +version = "2.6.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, ] [[package]] @@ -415,63 +609,71 @@ wheels = [ [[package]] name = "isort" -version = "7.0.0" +version = "8.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, ] [[package]] name = "librt" -version = "0.7.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, - { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, - { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, - { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, - { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, - { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, - { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, - { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, - { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, - { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, - { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, - { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, - { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, - { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, - { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, - { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, - { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, - { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, - { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, - { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, - { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, - { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, - { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, - { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, - { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, - { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, - { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, - { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, - { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, - { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] [[package]] @@ -611,45 +813,45 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, ] [[package]] name = "opentelemetry-exporter-otlp" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/9c/3ab1db90f32da200dba332658f2bbe602369e3d19f6aba394031a42635be/opentelemetry_exporter_otlp-1.39.1.tar.gz", hash = "sha256:7cf7470e9fd0060c8a38a23e4f695ac686c06a48ad97f8d4867bc9b420180b9c", size = 6147, upload-time = "2025-12-11T13:32:40.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/37/b6708e0eff5c5fb9aba2e0ea09f7f3bcbfd12a592d2a780241b5f6014df7/opentelemetry_exporter_otlp-1.40.0.tar.gz", hash = "sha256:7caa0870b95e2fcb59d64e16e2b639ecffb07771b6cd0000b5d12e5e4fef765a", size = 6152, upload-time = "2026-03-04T14:17:23.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/6c/bdc82a066e6fb1dcf9e8cc8d4e026358fe0f8690700cc6369a6bf9bd17a7/opentelemetry_exporter_otlp-1.39.1-py3-none-any.whl", hash = "sha256:68ae69775291f04f000eb4b698ff16ff685fdebe5cb52871bc4e87938a7b00fe", size = 7019, upload-time = "2025-12-11T13:32:19.387Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fc/aea77c28d9f3ffef2fdafdc3f4a235aee4091d262ddabd25882f47ce5c5f/opentelemetry_exporter_otlp-1.40.0-py3-none-any.whl", hash = "sha256:48c87e539ec9afb30dc443775a1334cc5487de2f72a770a4c00b1610bf6c697d", size = 7023, upload-time = "2026-03-04T14:17:03.612Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -660,14 +862,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/48/b329fed2c610c2c32c9366d9dc597202c9d1e58e631c137ba15248d8850f/opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad", size = 24650, upload-time = "2025-12-11T13:32:41.429Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/b9e60435cfcc7590fa87436edad6822240dddbc184643a2a005301cc31f4/opentelemetry_exporter_otlp_proto_grpc-1.40.0.tar.gz", hash = "sha256:bd4015183e40b635b3dab8da528b27161ba83bf4ef545776b196f0fb4ec47740", size = 25759, upload-time = "2026-03-04T14:17:24.4Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" }, + { url = "https://files.pythonhosted.org/packages/96/6f/7ee0980afcbdcd2d40362da16f7f9796bd083bf7f0b8e038abfbc0300f5d/opentelemetry_exporter_otlp_proto_grpc-1.40.0-py3-none-any.whl", hash = "sha256:2aa0ca53483fe0cf6405087a7491472b70335bc5c7944378a0a8e72e86995c52", size = 20304, upload-time = "2026-03-04T14:17:05.942Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -678,48 +880,103 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, +] + +[[package]] +name = "opentelemetry-instrumentation-logging" +version = "0.61b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/69473f925acfe2d4edf5c23bcced36906ac3627aa7c5722a8e3f60825f3b/opentelemetry_instrumentation_logging-0.61b0.tar.gz", hash = "sha256:feaa30b700acd2a37cc81db5f562ab0c3a5b6cc2453595e98b72c01dcf649584", size = 17906, upload-time = "2026-03-04T14:20:37.398Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0e/2137db5239cc5e564495549a4d11488a7af9b48fc76520a0eea20e69ddae/opentelemetry_instrumentation_logging-0.61b0-py3-none-any.whl", hash = "sha256:6d87e5ded6a0128d775d41511f8380910a1b610671081d16efb05ac3711c0074", size = 17076, upload-time = "2026-03-04T14:19:36.765Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.39.1" +version = "1.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.60b1" +version = "0.61b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, + { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, +] + +[[package]] +name = "oracledb" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/02/70a872d1a4a739b4f7371ab8d3d5ed8c6e57e142e2503531aafcb220893c/oracledb-3.4.2.tar.gz", hash = "sha256:46e0f2278ff1fe83fbc33a3b93c72d429323ec7eed47bc9484e217776cd437e5", size = 855467, upload-time = "2026-01-28T17:25:39.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/81/2e6154f34b71cd93b4946c73ea13b69d54b8d45a5f6bbffe271793240d21/oracledb-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a7396664e592881225ba66385ee83ce339d864f39003d6e4ca31a894a7e7c552", size = 4220806, upload-time = "2026-01-28T17:26:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a9/a1d59aaac77d8f727156ec6a3b03399917c90b7da4f02d057f92e5601f56/oracledb-3.4.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f04a2d62073407672f114d02529921de0677c6883ed7c64d8d1a3c04caa3238", size = 2233795, upload-time = "2026-01-28T17:26:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/94/ec/8c4a38020cd251572bd406ddcbde98ca052ec94b5684f9aa9ef1ddfcc68c/oracledb-3.4.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8d75e4f879b908be66cce05ba6c05791a5dbb4a15e39abc01aa25c8a2492bd9", size = 2424756, upload-time = "2026-01-28T17:26:07.35Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7d/c251c2a8567151ccfcfbe3467ea9a60fb5480dc4719342e2e6b7a9679e5d/oracledb-3.4.2-cp312-cp312-win32.whl", hash = "sha256:31b7ee83c23d0439778303de8a675717f805f7e8edb5556d48c4d8343bcf14f5", size = 1453486, upload-time = "2026-01-28T17:26:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/4c/78/c939f3c16fb39400c4734d5a3340db5659ba4e9dce23032d7b33ccfd3fe5/oracledb-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:ac25a0448fc830fb7029ad50cd136cdbfcd06975d53967e269772cc5cb8c203a", size = 1794445, upload-time = "2026-01-28T17:26:10.66Z" }, + { url = "https://files.pythonhosted.org/packages/22/68/f7126f5d911c295b57720c6b1a0609a5a2667b4546946433552a4de46333/oracledb-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:643c25d301a289a371e37fcedb59e5fa5e54fb321708e5c12821c4b55bdd8a4d", size = 4205176, upload-time = "2026-01-28T17:26:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5d/93/2fced60f92dc82e66980a8a3ba5c1ea48110bf1dd81d030edb69d88f992e/oracledb-3.4.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55397e7eb43bb7017c03a981c736c25724182f5210951181dfe3fab0e5d457fb", size = 2231298, upload-time = "2026-01-28T17:26:14.497Z" }, + { url = "https://files.pythonhosted.org/packages/75/a7/4dd286f3a6348d786fef9e6ab2e6c9b74ca9195d9a756f2a67e45743cdf0/oracledb-3.4.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26a10f9c790bd141ffc8af68520803ed4a44a9258bf7d1eea9bfdd36bd6df7f", size = 2439430, upload-time = "2026-01-28T17:26:16.044Z" }, + { url = "https://files.pythonhosted.org/packages/19/28/94bc753e5e969c60ee5d9c914e2b4ef79999eaca8e91bcab2fbf0586b80b/oracledb-3.4.2-cp313-cp313-win32.whl", hash = "sha256:b974caec2c330c22bbe765705a5ac7d98ec3022811dec2042d561a3c65cb991b", size = 1458209, upload-time = "2026-01-28T17:26:17.652Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/593a9b2d4c12c9de3289e67d84fe023336d99f36ba51442a5a0f5ce6acf7/oracledb-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:3df8eee1410d25360599968b1625b000f10c5ae0e47274031a7842a9dc418890", size = 1793558, upload-time = "2026-01-28T17:26:19.914Z" }, + { url = "https://files.pythonhosted.org/packages/42/20/1e98f84c1555911c46b4fa870fbef2a80617bf7e0a5f178078ecf466c917/oracledb-3.4.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:59ad6438f56a25e8e1a4a3dd1b42235a5d09ab9ba417ff2ad14eae6596f3d06f", size = 4247459, upload-time = "2026-01-28T17:26:22.356Z" }, + { url = "https://files.pythonhosted.org/packages/7d/74/95963e2d94f84b9937a562a9a2529f72d050afbc2ffd88f6661e3a876f7d/oracledb-3.4.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:404ec1451d0448653ee074213b87d6c5bd65eaa74b50083ddf2c9c3e11c71c71", size = 2271749, upload-time = "2026-01-28T17:26:24.078Z" }, + { url = "https://files.pythonhosted.org/packages/82/89/38ce85148a246087795379ee52c5b20726a00a69c87ba6ec266bcdad30fc/oracledb-3.4.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19fa80ef84f85ad74077aa626067bbe697e527bd39604b4209f9d86cb2876b89", size = 2452031, upload-time = "2026-01-28T17:26:26.08Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/51fe907fdec0267ad7c6e9a62998cbe878efcd168ea6e39f162fab62fdaa/oracledb-3.4.2-cp314-cp314-win32.whl", hash = "sha256:d7ce75c498bff758548ec6e4424ab4271aa257e5887cc436a54bc947fd46199a", size = 1480973, upload-time = "2026-01-28T17:26:27.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/22/a37354f19786774e5e4041338043b516db060aacfdfcd5aca8bb92c2539a/oracledb-3.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:5d7befb014174c5ae11c3a08f5ed6668a25ab2335d8e7104dca70d54d54a5b3a", size = 1837756, upload-time = "2026-01-28T17:26:29.032Z" }, ] [[package]] @@ -742,11 +999,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.5.1" +version = "4.9.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, ] [[package]] @@ -776,30 +1033,30 @@ wheels = [ [[package]] name = "protobuf" -version = "6.33.5" +version = "6.33.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, - { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, - { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, + { url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" }, + { url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" }, + { url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] [[package]] name = "psycopg" -version = "3.3.2" +version = "3.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" }, ] [package.optional-dependencies] @@ -809,42 +1066,51 @@ binary = [ [[package]] name = "psycopg-binary" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/1e/8614b01c549dd7e385dacdcd83fe194f6b3acb255a53cc67154ee6bf00e7/psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634", size = 4579832, upload-time = "2025-12-06T17:33:01.388Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/0bb093570fae2f4454d42c1ae6000f15934391867402f680254e4a7def54/psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688", size = 4658786, upload-time = "2025-12-06T17:33:05.022Z" }, - { url = "https://files.pythonhosted.org/packages/61/20/1d9383e3f2038826900a14137b0647d755f67551aab316e1021443105ed5/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4", size = 5454896, upload-time = "2025-12-06T17:33:09.023Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/513c80ad8bbb545e364f7737bf2492d34a4c05eef4f7b5c16428dc42260d/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d45acedcaa58619355f18e0f42af542fcad3fd84ace4b8355d3a5dea23318578", size = 5132731, upload-time = "2025-12-06T17:33:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/f3/28/ddf5f5905f088024bccb19857949467407c693389a14feb527d6171d8215/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d88f32ff8c47cb7f4e7e7a9d1747dcee6f3baa19ed9afa9e5694fd2fb32b61ed", size = 6724495, upload-time = "2025-12-06T17:33:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/6e/93/a1157ebcc650960b264542b547f7914d87a42ff0cc15a7584b29d5807e6b/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59d0163c4617a2c577cb34afbed93d7a45b8c8364e54b2bd2020ff25d5f5f860", size = 4964979, upload-time = "2025-12-06T17:33:20.179Z" }, - { url = "https://files.pythonhosted.org/packages/0e/27/65939ba6798f9c5be4a5d9cd2061ebaf0851798525c6811d347821c8132d/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e750afe74e6c17b2c7046d2c3e3173b5a3f6080084671c8aa327215323df155b", size = 4493648, upload-time = "2025-12-06T17:33:23.464Z" }, - { url = "https://files.pythonhosted.org/packages/8a/c4/5e9e4b9b1c1e27026e43387b0ba4aaf3537c7806465dd3f1d5bde631752a/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f26f113013c4dcfbfe9ced57b5bad2035dda1a7349f64bf726021968f9bccad3", size = 4173392, upload-time = "2025-12-06T17:33:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/c6/81/cf43fb76993190cee9af1cbcfe28afb47b1928bdf45a252001017e5af26e/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca", size = 3909241, upload-time = "2025-12-06T17:33:30.092Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/c6377a0d17434674351627489deca493ea0b137c522b99c81d3a106372c8/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12", size = 4219746, upload-time = "2025-12-06T17:33:33.097Z" }, - { url = "https://files.pythonhosted.org/packages/25/32/716c57b28eefe02a57a4c9d5bf956849597f5ea476c7010397199e56cfde/psycopg_binary-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf", size = 3537494, upload-time = "2025-12-06T17:33:35.82Z" }, - { url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" }, - { url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" }, - { url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" }, - { url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" }, - { url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" }, - { url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" }, - { url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" }, - { url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" }, - { url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" }, - { url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" }, - { url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" }, - { url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" }, - { url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" }, - { url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" }, - { url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" }, - { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" }, +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/15/021be5c0cbc5b7c1ab46e91cc3434eb42569f79a0592e67b8d25e66d844d/psycopg_binary-3.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6698dbab5bcef8fdb570fc9d35fd9ac52041771bfcfe6fd0fc5f5c4e36f1e99d", size = 4591170, upload-time = "2026-02-18T16:48:55.594Z" }, + { url = "https://files.pythonhosted.org/packages/f1/54/a60211c346c9a2f8c6b272b5f2bbe21f6e11800ce7f61e99ba75cf8b63e1/psycopg_binary-3.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:329ff393441e75f10b673ae99ab45276887993d49e65f141da20d915c05aafd8", size = 4670009, upload-time = "2026-02-18T16:49:03.608Z" }, + { url = "https://files.pythonhosted.org/packages/c1/53/ac7c18671347c553362aadbf65f92786eef9540676ca24114cc02f5be405/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eb072949b8ebf4082ae24289a2b0fd724da9adc8f22743409d6fd718ddb379df", size = 5469735, upload-time = "2026-02-18T16:49:10.128Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c3/4f4e040902b82a344eff1c736cde2f2720f127fe939c7e7565706f96dd44/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:263a24f39f26e19ed7fc982d7859a36f17841b05bebad3eb47bb9cd2dd785351", size = 5152919, upload-time = "2026-02-18T16:49:16.335Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/d929679c6a5c212bcf738806c7c89f5b3d0919f2e1685a0e08d6ff877945/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5152d50798c2fa5bd9b68ec68eb68a1b71b95126c1d70adaa1a08cd5eefdc23d", size = 6738785, upload-time = "2026-02-18T16:49:22.687Z" }, + { url = "https://files.pythonhosted.org/packages/69/b0/09703aeb69a9443d232d7b5318d58742e8ca51ff79f90ffe6b88f1db45e7/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d6a1e56dd267848edb824dbeb08cf5bac649e02ee0b03ba883ba3f4f0bd54f2", size = 4979008, upload-time = "2026-02-18T16:49:27.313Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/e662558b793c6e13a7473b970fee327d635270e41eded3090ef14045a6a5/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73eaaf4bb04709f545606c1db2f65f4000e8a04cdbf3e00d165a23004692093e", size = 4508255, upload-time = "2026-02-18T16:49:31.575Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7f/0f8b2e1d5e0093921b6f324a948a5c740c1447fbb45e97acaf50241d0f39/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:162e5675efb4704192411eaf8e00d07f7960b679cd3306e7efb120bb8d9456cc", size = 4189166, upload-time = "2026-02-18T16:49:35.801Z" }, + { url = "https://files.pythonhosted.org/packages/92/ec/ce2e91c33bc8d10b00c87e2f6b0fb570641a6a60042d6a9ae35658a3a797/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:fab6b5e37715885c69f5d091f6ff229be71e235f272ebaa35158d5a46fd548a0", size = 3924544, upload-time = "2026-02-18T16:49:41.129Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2f/7718141485f73a924205af60041c392938852aa447a94c8cbd222ff389a1/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4aab31bd6d1057f287c96c0effca3a25584eb9cc702f282ecb96ded7814e830", size = 4235297, upload-time = "2026-02-18T16:49:46.726Z" }, + { url = "https://files.pythonhosted.org/packages/57/f9/1add717e2643a003bbde31b1b220172e64fbc0cb09f06429820c9173f7fc/psycopg_binary-3.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:59aa31fe11a0e1d1bcc2ce37ed35fe2ac84cd65bb9036d049b1a1c39064d0f14", size = 3547659, upload-time = "2026-02-18T16:49:52.999Z" }, + { url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" }, + { url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" }, + { url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" }, + { url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" }, + { url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" }, + { url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" }, + { url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" }, + { url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" }, + { url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" }, + { url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -935,11 +1201,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] @@ -960,6 +1226,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/6f/9ac2548e290764781f9e7e2aaf0685b086379dabfb29ca38536985471eaf/pylint-4.0.5-py3-none-any.whl", hash = "sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2", size = 536694, upload-time = "2026-02-20T09:07:31.028Z" }, ] +[[package]] +name = "pymysql" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, +] + +[[package]] +name = "pyodbc" +version = "5.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/85/44b10070a769a56bd910009bb185c0c0a82daff8d567cd1a116d7d730c7d/pyodbc-5.3.0.tar.gz", hash = "sha256:2fe0e063d8fb66efd0ac6dc39236c4de1a45f17c33eaded0d553d21c199f4d05", size = 121770, upload-time = "2025-10-17T18:04:09.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/0c/7ecf8077f4b932a5d25896699ff5c394ffc2a880a9c2c284d6a3e6ea5949/pyodbc-5.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5ebf6b5d989395efe722b02b010cb9815698a4d681921bf5db1c0e1195ac1bde", size = 72994, upload-time = "2025-10-17T18:03:20.551Z" }, + { url = "https://files.pythonhosted.org/packages/03/78/9fbde156055d88c1ef3487534281a5b1479ee7a2f958a7e90714968749ac/pyodbc-5.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:197bb6ddafe356a916b8ee1b8752009057fce58e216e887e2174b24c7ab99269", size = 72535, upload-time = "2025-10-17T18:03:21.423Z" }, + { url = "https://files.pythonhosted.org/packages/9f/f9/8c106dcd6946e95fee0da0f1ba58cd90eb872eebe8968996a2ea1f7ac3c1/pyodbc-5.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6ccb5315ec9e081f5cbd66f36acbc820ad172b8fa3736cf7f993cdf69bd8a96", size = 333565, upload-time = "2025-10-17T18:03:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/2c70f47a76a4fafa308d148f786aeb35a4d67a01d41002f1065b465d9994/pyodbc-5.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5dd3d5e469f89a3112cf8b0658c43108a4712fad65e576071e4dd44d2bd763c7", size = 340283, upload-time = "2025-10-17T18:03:23.691Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b2/0631d84731606bfe40d3b03a436b80cbd16b63b022c7b13444fb30761ca8/pyodbc-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b180bc5e49b74fd40a24ef5b0fe143d0c234ac1506febe810d7434bf47cb925b", size = 1302767, upload-time = "2025-10-17T18:03:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/74/b9/707c5314cca9401081b3757301241c167a94ba91b4bd55c8fa591bf35a4a/pyodbc-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3c39de3005fff3ae79246f952720d44affc6756b4b85398da4c5ea76bf8f506", size = 1361251, upload-time = "2025-10-17T18:03:26.538Z" }, + { url = "https://files.pythonhosted.org/packages/97/7c/893036c8b0c8d359082a56efdaa64358a38dda993124162c3faa35d1924d/pyodbc-5.3.0-cp312-cp312-win32.whl", hash = "sha256:d32c3259762bef440707098010035bbc83d1c73d81a434018ab8c688158bd3bb", size = 63413, upload-time = "2025-10-17T18:03:27.903Z" }, + { url = "https://files.pythonhosted.org/packages/c0/70/5e61b216cc13c7f833ef87f4cdeab253a7873f8709253f5076e9bb16c1b3/pyodbc-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe77eb9dcca5fc1300c9121f81040cc9011d28cff383e2c35416e9ec06d4bc95", size = 70133, upload-time = "2025-10-17T18:03:28.746Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/e7d0629c9714a85eb4f85d21602ce6d8a1ec0f313fde8017990cf913e3b4/pyodbc-5.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:afe7c4ac555a8d10a36234788fc6cfc22a86ce37fc5ba88a1f75b3e6696665dc", size = 64700, upload-time = "2025-10-17T18:03:29.638Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/9e74cbcc1d4878553eadfd59138364b38656369eb58f7e5b42fb344c0ce7/pyodbc-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e9ab0b91de28a5ab838ac4db0253d7cc8ce2452efe4ad92ee6a57b922bf0c24", size = 72975, upload-time = "2025-10-17T18:03:30.466Z" }, + { url = "https://files.pythonhosted.org/packages/37/c7/27d83f91b3144d3e275b5b387f0564b161ddbc4ce1b72bb3b3653e7f4f7a/pyodbc-5.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6132554ffbd7910524d643f13ce17f4a72f3a6824b0adef4e9a7f66efac96350", size = 72541, upload-time = "2025-10-17T18:03:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/2bb24e7fc95e98a7b11ea5ad1f256412de35d2e9cc339be198258c1d9a76/pyodbc-5.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1629af4706e9228d79dabb4863c11cceb22a6dab90700db0ef449074f0150c0d", size = 343287, upload-time = "2025-10-17T18:03:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/fa/24/88cde8b6dc07a93a92b6c15520a947db24f55db7bd8b09e85956642b7cf3/pyodbc-5.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ceaed87ba2ea848c11223f66f629ef121f6ebe621f605cde9cfdee4fd9f4b68", size = 350094, upload-time = "2025-10-17T18:03:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/99/53c08562bc171a618fa1699297164f8885e66cde38c3b30f454730d0c488/pyodbc-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3cc472c8ae2feea5b4512e23b56e2b093d64f7cbc4b970af51da488429ff7818", size = 1301029, upload-time = "2025-10-17T18:03:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/d8/10/68a0b5549876d4b53ba4c46eed2a7aca32d589624ed60beef5bd7382619e/pyodbc-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c79df54bbc25bce9f2d87094e7b39089c28428df5443d1902b0cc5f43fd2da6f", size = 1361420, upload-time = "2025-10-17T18:03:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/41/0f/9dfe4987283ffcb981c49a002f0339d669215eb4a3fe4ee4e14537c52852/pyodbc-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c2eb0b08e24fe5c40c7ebe9240c5d3bd2f18cd5617229acee4b0a0484dc226f2", size = 63399, upload-time = "2025-10-17T18:03:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/56/03/15dcefe549d3888b649652af7cca36eda97c12b6196d92937ca6d11306e9/pyodbc-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:01166162149adf2b8a6dc21a212718f205cabbbdff4047dc0c415af3fd85867e", size = 70133, upload-time = "2025-10-17T18:03:38.47Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c1/c8b128ae59a14ecc8510e9b499208e342795aecc3af4c3874805c720b8db/pyodbc-5.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:363311bd40320b4a61454bebf7c38b243cd67c762ed0f8a5219de3ec90c96353", size = 64683, upload-time = "2025-10-17T18:03:39.68Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/c26d82a7ce1e90b8bbb8731d3d53de73814e2f6606b9db9d978303aa8d5f/pyodbc-5.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3f1bdb3ce6480a17afaaef4b5242b356d4997a872f39e96f015cabef00613797", size = 73513, upload-time = "2025-10-17T18:03:40.536Z" }, + { url = "https://files.pythonhosted.org/packages/82/d5/1ab1b7c4708cbd701990a8f7183c5bb5e0712d5e8479b919934e46dadab4/pyodbc-5.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7713c740a10f33df3cb08f49a023b7e1e25de0c7c99650876bbe717bc95ee780", size = 72631, upload-time = "2025-10-17T18:03:41.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/7e3831eeac2b09b31a77e6b3495491ce162035ff2903d7261b49d35aa3c2/pyodbc-5.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cf18797a12e70474e1b7f5027deeeccea816372497e3ff2d46b15bec2d18a0cc", size = 344580, upload-time = "2025-10-17T18:03:42.67Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a6/71d26d626a3c45951620b7ff356ec920e420f0e09b0a924123682aa5e4ab/pyodbc-5.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:08b2439500e212625471d32f8fde418075a5ddec556e095e5a4ba56d61df2dc6", size = 350224, upload-time = "2025-10-17T18:03:43.731Z" }, + { url = "https://files.pythonhosted.org/packages/93/14/f702c5e8c2d595776266934498505f11b7f1545baf21ffec1d32c258e9d3/pyodbc-5.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:729c535341bb09c476f219d6f7ab194bcb683c4a0a368010f1cb821a35136f05", size = 1301503, upload-time = "2025-10-17T18:03:45.013Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b2/ad92ebdd1b5c7fec36b065e586d1d34b57881e17ba5beec5c705f1031058/pyodbc-5.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c67e7f2ce649155ea89beb54d3b42d83770488f025cf3b6f39ca82e9c598a02e", size = 1361050, upload-time = "2025-10-17T18:03:46.298Z" }, + { url = "https://files.pythonhosted.org/packages/19/40/dc84e232da07056cb5aaaf5f759ba4c874bc12f37569f7f1670fc71e7ae1/pyodbc-5.3.0-cp314-cp314-win32.whl", hash = "sha256:a48d731432abaee5256ed6a19a3e1528b8881f9cb25cb9cf72d8318146ea991b", size = 65670, upload-time = "2025-10-17T18:03:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/b8/79/c48be07e8634f764662d7a279ac204f93d64172162dbf90f215e2398b0bd/pyodbc-5.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:58635a1cc859d5af3f878c85910e5d7228fe5c406d4571bffcdd281375a54b39", size = 72177, upload-time = "2025-10-17T18:03:57.296Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/e304574446b2263f428ce14df590ba52c2e0e0205e8d34b235b582b7d57e/pyodbc-5.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:754d052030d00c3ac38da09ceb9f3e240e8dd1c11da8906f482d5419c65b9ef5", size = 66668, upload-time = "2025-10-17T18:03:58.174Z" }, + { url = "https://files.pythonhosted.org/packages/43/17/f4eabf443b838a2728773554017d08eee3aca353102934a7e3ba96fb0e31/pyodbc-5.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f927b440c38ade1668f0da64047ffd20ec34e32d817f9a60d07553301324b364", size = 75780, upload-time = "2025-10-17T18:03:47.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/ea/e79e168c3d38c27d59d5d96273fd9e3c3ba55937cc944c4e60618f51de90/pyodbc-5.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:25c4cfb2c08e77bc6e82f666d7acd52f0e52a0401b1876e60f03c73c3b8aedc0", size = 75503, upload-time = "2025-10-17T18:03:48.171Z" }, + { url = "https://files.pythonhosted.org/packages/90/81/d1d7c125ec4a20e83fdc28e119b8321192b2bd694f432cf63e1199b2b929/pyodbc-5.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc834567c2990584b9726cba365834d039380c9dbbcef3030ddeb00c6541b943", size = 398356, upload-time = "2025-10-17T18:03:49.131Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fc/f6be4b3cc3910f8c2aba37aa41671121fd6f37b402ae0fefe53a70ac7cd5/pyodbc-5.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8339d3094858893c1a68ee1af93efc4dff18b8b65de54d99104b99af6306320d", size = 397291, upload-time = "2025-10-17T18:03:50.18Z" }, + { url = "https://files.pythonhosted.org/packages/03/2e/0610b1ed05a5625528d52f6cece9610e84617d35f475c89c2a52f66d13f7/pyodbc-5.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74528fe148980d0c735c0ebb4a4dc74643ac4574337c43c1006ac4d09593f92d", size = 1353900, upload-time = "2025-10-17T18:03:51.339Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f1/43497e1d37f9f71b43b2b3172e7b1bdf50851e278390c3fb6b46a3630c53/pyodbc-5.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d89a7f2e24227150c13be8164774b7e1f9678321a4248f1356a465b9cc17d31e", size = 1406062, upload-time = "2025-10-17T18:03:52.546Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/88a1277c2f7d9ab1cec0a71e074ba24fd4a1710a43974682546da90a1343/pyodbc-5.3.0-cp314-cp314t-win32.whl", hash = "sha256:af4d8c9842fc4a6360c31c35508d6594d5a3b39922f61b282c2b4c9d9da99514", size = 70132, upload-time = "2025-10-17T18:03:53.715Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/ee98c62050de4aa8bafb6eb1e11b95e0b0c898bd5930137c6dc776e06a9b/pyodbc-5.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bfeb3e34795d53b7d37e66dd54891d4f9c13a3889a8f5fe9640e56a82d770955", size = 79452, upload-time = "2025-10-17T18:03:54.664Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8f/d8889efd96bbe8e5d43ff9701f6b1565a8e09c3e1f58c388d550724f777b/pyodbc-5.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:13656184faa3f2d5c6f19b701b8f247342ed581484f58bf39af7315c054e69db", size = 70142, upload-time = "2025-10-17T18:03:55.551Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -991,16 +1310,16 @@ wheels = [ [[package]] name = "pytest-cov" -version = "7.0.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage" }, { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -1028,6 +1347,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] +[[package]] +name = "python-discovery" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/88/815e53084c5079a59df912825a279f41dd2e0df82281770eadc732f5352c/python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e", size = 58457, upload-time = "2026-03-26T22:30:44.496Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502", size = 31674, upload-time = "2026-03-26T22:30:43.396Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1085,7 +1417,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1093,9 +1425,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -1112,49 +1444,50 @@ wheels = [ [[package]] name = "rich" -version = "14.3.2" +version = "14.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] name = "ruff" -version = "0.15.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, - { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, - { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, - { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, - { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, - { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, - { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, - { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, - { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, - { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, +version = "0.15.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/b0/73cf7550861e2b4824950b8b52eebdcc5adc792a00c514406556c5b80817/ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e", size = 4610921, upload-time = "2026-03-26T18:39:38.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/92/c445b0cd6da6e7ae51e954939cb69f97e008dbe750cfca89b8cedc081be7/ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7", size = 10527394, upload-time = "2026-03-26T18:39:41.566Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/f1c662784d149ad1414cae450b082cf736430c12ca78367f20f5ed569d65/ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570", size = 10905693, upload-time = "2026-03-26T18:39:30.364Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/7a631a8af6d88bcef997eb1bf87cc3da158294c57044aafd3e17030613de/ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3", size = 10323044, upload-time = "2026-03-26T18:39:33.37Z" }, + { url = "https://files.pythonhosted.org/packages/67/18/1bf38e20914a05e72ef3b9569b1d5c70a7ef26cd188d69e9ca8ef588d5bf/ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94", size = 10629135, upload-time = "2026-03-26T18:39:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e9/138c150ff9af60556121623d41aba18b7b57d95ac032e177b6a53789d279/ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3", size = 10348041, upload-time = "2026-03-26T18:39:52.178Z" }, + { url = "https://files.pythonhosted.org/packages/02/f1/5bfb9298d9c323f842c5ddeb85f1f10ef51516ac7a34ba446c9347d898df/ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762", size = 11121987, upload-time = "2026-03-26T18:39:55.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/11/6da2e538704e753c04e8d86b1fc55712fdbdcc266af1a1ece7a51fff0d10/ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a", size = 11951057, upload-time = "2026-03-26T18:39:19.18Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/c9208c5fd5101bf87002fed774ff25a96eea313d305f1e5d5744698dc314/ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8", size = 11464613, upload-time = "2026-03-26T18:40:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/22/d7f2fabdba4fae9f3b570e5605d5eb4500dcb7b770d3217dca4428484b17/ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1", size = 11257557, upload-time = "2026-03-26T18:39:57.972Z" }, + { url = "https://files.pythonhosted.org/packages/71/8c/382a9620038cf6906446b23ce8632ab8c0811b8f9d3e764f58bedd0c9a6f/ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec", size = 11169440, upload-time = "2026-03-26T18:39:22.205Z" }, + { url = "https://files.pythonhosted.org/packages/4d/0d/0994c802a7eaaf99380085e4e40c845f8e32a562e20a38ec06174b52ef24/ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6", size = 10605963, upload-time = "2026-03-26T18:39:46.682Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/d624b86f5b0aad7cef6bbf9cd47a6a02dfdc4f72c92a337d724e39c9d14b/ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb", size = 10357484, upload-time = "2026-03-26T18:39:49.176Z" }, + { url = "https://files.pythonhosted.org/packages/35/c3/e0b7835d23001f7d999f3895c6b569927c4d39912286897f625736e1fd04/ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8", size = 10830426, upload-time = "2026-03-26T18:40:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/ab20b322f637b369383adc341d761eaaa0f0203d6b9a7421cd6e783d81b9/ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49", size = 11345125, upload-time = "2026-03-26T18:39:27.799Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/90b2b33419f59d0f2c4c8a48a4b74b460709a557e8e0064cf33ad894f983/ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34", size = 10571959, upload-time = "2026-03-26T18:39:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a2/ef467cb77099062317154c63f234b8a7baf7cb690b99af760c5b68b9ee7f/ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89", size = 11743893, upload-time = "2026-03-26T18:39:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/77be4fff062fa78d9b2a4dea85d14785dac5f1d0c1fb58ed52331f0ebe28/ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2", size = 11048175, upload-time = "2026-03-26T18:40:01.06Z" }, ] [[package]] name = "shared" -version = "8.3.2" -source = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fshared&branch=main#7d6c7b7b7a0cbe90b6ae68bc9c26feb298e8e0e4" } +version = "2.6.2.dev12+g4a93a5d9c" +source = { git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fshared&branch=main#4a93a5d9c140de1e2dbc5f3be6901943b0baaea7" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-instrumentation-logging" }, { name = "opentelemetry-sdk" }, { name = "pydantic" }, { name = "pyyaml" }, @@ -1164,31 +1497,90 @@ dependencies = [ name = "sql-to-arc" source = { editable = "middleware/sql_to_arc" } dependencies = [ + { name = "aiomysql" }, + { name = "aioodbc" }, { name = "api-client" }, { name = "arctrl" }, { name = "opentelemetry-api" }, + { name = "oracledb" }, { name = "psycopg", extra = ["binary"] }, { name = "pydantic" }, { name = "shared" }, + { name = "sqlalchemy", extra = ["asyncio"] }, ] [package.metadata] requires-dist = [ + { name = "aiomysql", specifier = ">=0.2.0" }, + { name = "aioodbc", specifier = ">=0.4.1" }, { name = "api-client", git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fapi_client&branch=main" }, { name = "arctrl", specifier = ">=3.0.0b15" }, - { name = "opentelemetry-api", specifier = ">=1.39.1" }, - { name = "psycopg", extras = ["binary"], specifier = ">=3.3.2" }, + { name = "opentelemetry-api", specifier = ">=1.30.0" }, + { name = "oracledb", specifier = ">=2.0.0" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.3.3" }, { name = "pydantic", specifier = ">=2.12.5" }, { name = "shared", git = "https://github.com/fairagro/m4.2_advanced_middleware_api.git?subdirectory=middleware%2Fshared&branch=main" }, + { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.46" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, + { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, + { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, + { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, + { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, + { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, + { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, + { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, + { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, + { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, ] [[package]] name = "stevedore" -version = "5.6.0" +version = "5.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6d/90764092216fa560f6587f83bb70113a8ba510ba436c6476a2b47359057c/stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3", size = 516200, upload-time = "2026-02-20T13:27:06.765Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" }, + { url = "https://files.pythonhosted.org/packages/69/06/36d260a695f383345ab5bbc3fd447249594ae2fa8dfd19c533d5ae23f46b/stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed", size = 54483, upload-time = "2026-02-20T13:27:05.561Z" }, ] [[package]] @@ -1241,16 +1633,66 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.36.1" +version = "21.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, +] + +[[package]] +name = "wrapt" +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] [[package]]