diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f01e5b5f4..aac4f6180 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,12 +1,12 @@ { - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "generate-api", - "path": "frontend", - "problemMatcher": [], - "label": "Generate frontend code from OpenAPI" - } - ] + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "generate:api", + "path": "frontend", + "problemMatcher": [], + "label": "Generate frontend code from OpenAPI" + } + ] } diff --git a/README.md b/README.md index 27f8e6469..a2b8efbe1 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ e.g. typically when a new dependency is added, the relevant component needs to b All the content in `/frontend/src/api/autogen` is auto-generated using the defined endpoints in the Python backend. In order to update the auto-generated code you can either -1. Run `npm run generate-api --prefix ./frontend`. +1. Run `npm run generate:api --prefix ./frontend`. 2. Use the VSCode tasks shortcut: a) `Ctrl + P` to open the command palette. b) Type `> Tasks` and enter to filter to commands only. diff --git a/backend_py/primary/poetry.lock b/backend_py/primary/poetry.lock index d67e1b38d..5e5a2c52e 100644 --- a/backend_py/primary/poetry.lock +++ b/backend_py/primary/poetry.lock @@ -1,5 +1,141 @@ # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1"}, + {file = "aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a"}, + {file = "aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685"}, + {file = "aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b"}, + {file = "aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3"}, + {file = "aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1"}, + {file = "aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51"}, + {file = "aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0"}, + {file = "aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09"}, + {file = "aiohttp-3.12.15-cp39-cp39-win32.whl", hash = "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d"}, + {file = "aiohttp-3.12.15-cp39-cp39-win_amd64.whl", hash = "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8"}, + {file = "aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + [[package]] name = "annotated-types" version = "0.7.0" @@ -78,6 +214,26 @@ files = [ {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, ] +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] + [[package]] name = "azure-core" version = "1.35.0" @@ -115,6 +271,22 @@ files = [ azure-core = ">=1.24.0" opentelemetry-api = ">=1.12.0" +[[package]] +name = "azure-cosmos" +version = "4.9.0" +description = "Microsoft Azure Cosmos Client Library for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "azure_cosmos-4.9.0-py3-none-any.whl", hash = "sha256:3b60eaa01a16a857d0faf0cec304bac6fa8620a81bc268ce760339032ef617fe"}, + {file = "azure_cosmos-4.9.0.tar.gz", hash = "sha256:c70db4cbf55b0ff261ed7bb8aa325a5dfa565d3c6eaa43d75d26ae5e2ad6d74f"}, +] + +[package.dependencies] +azure-core = ">=1.30.0" +typing-extensions = ">=4.6.0" + [[package]] name = "azure-identity" version = "1.23.0" @@ -922,6 +1094,120 @@ ufo = ["fs (>=2.2.0,<3)"] unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""] woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] +[[package]] +name = "frozenlist" +version = "1.7.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718"}, + {file = "frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e"}, + {file = "frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56"}, + {file = "frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7"}, + {file = "frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}, + {file = "frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}, + {file = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e"}, + {file = "frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1"}, + {file = "frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e"}, + {file = "frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63"}, + {file = "frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}, + {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, +] + [[package]] name = "h11" version = "0.16.0" @@ -1625,6 +1911,126 @@ requests-oauthlib = ">=0.5.0" [package.extras] async = ["aiodns ; python_version >= \"3.5\"", "aiohttp (>=3.0) ; python_version >= \"3.5\""] +[[package]] +name = "multidict" +version = "6.6.3" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817"}, + {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140"}, + {file = "multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8"}, + {file = "multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61"}, + {file = "multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b"}, + {file = "multidict-6.6.3-cp310-cp310-win32.whl", hash = "sha256:20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318"}, + {file = "multidict-6.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485"}, + {file = "multidict-6.6.3-cp310-cp310-win_arm64.whl", hash = "sha256:769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5"}, + {file = "multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c"}, + {file = "multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df"}, + {file = "multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9"}, + {file = "multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56"}, + {file = "multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183"}, + {file = "multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5"}, + {file = "multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2"}, + {file = "multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb"}, + {file = "multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6"}, + {file = "multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f"}, + {file = "multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a"}, + {file = "multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75"}, + {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10"}, + {file = "multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5"}, + {file = "multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17"}, + {file = "multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b"}, + {file = "multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55"}, + {file = "multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b"}, + {file = "multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca"}, + {file = "multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1"}, + {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6"}, + {file = "multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e"}, + {file = "multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9"}, + {file = "multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600"}, + {file = "multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134"}, + {file = "multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37"}, + {file = "multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0"}, + {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d"}, + {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c"}, + {file = "multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e"}, + {file = "multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d"}, + {file = "multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb"}, + {file = "multidict-6.6.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c8161b5a7778d3137ea2ee7ae8a08cce0010de3b00ac671c5ebddeaa17cefd22"}, + {file = "multidict-6.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1328201ee930f069961ae707d59c6627ac92e351ed5b92397cf534d1336ce557"}, + {file = "multidict-6.6.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b1db4d2093d6b235de76932febf9d50766cf49a5692277b2c28a501c9637f616"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53becb01dd8ebd19d1724bebe369cfa87e4e7f29abbbe5c14c98ce4c383e16cd"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41bb9d1d4c303886e2d85bade86e59885112a7f4277af5ad47ab919a2251f306"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:775b464d31dac90f23192af9c291dc9f423101857e33e9ebf0020a10bfcf4144"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d04d01f0a913202205a598246cf77826fe3baa5a63e9f6ccf1ab0601cf56eca0"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d25594d3b38a2e6cabfdcafef339f754ca6e81fbbdb6650ad773ea9775af35ab"}, + {file = "multidict-6.6.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35712f1748d409e0707b165bf49f9f17f9e28ae85470c41615778f8d4f7d9609"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1c8082e5814b662de8589d6a06c17e77940d5539080cbab9fe6794b5241b76d9"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:61af8a4b771f1d4d000b3168c12c3120ccf7284502a94aa58c68a81f5afac090"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:448e4a9afccbf297577f2eaa586f07067441e7b63c8362a3540ba5a38dc0f14a"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:233ad16999afc2bbd3e534ad8dbe685ef8ee49a37dbc2cdc9514e57b6d589ced"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:bb933c891cd4da6bdcc9733d048e994e22e1883287ff7540c2a0f3b117605092"}, + {file = "multidict-6.6.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:37b09ca60998e87734699e88c2363abfd457ed18cfbf88e4009a4e83788e63ed"}, + {file = "multidict-6.6.3-cp39-cp39-win32.whl", hash = "sha256:f54cb79d26d0cd420637d184af38f0668558f3c4bbe22ab7ad830e67249f2e0b"}, + {file = "multidict-6.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:295adc9c0551e5d5214b45cf29ca23dbc28c2d197a9c30d51aed9e037cb7c578"}, + {file = "multidict-6.6.3-cp39-cp39-win_arm64.whl", hash = "sha256:15332783596f227db50fb261c2c251a58ac3873c457f3a550a95d5c0aa3c770d"}, + {file = "multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a"}, + {file = "multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc"}, +] + [[package]] name = "mypy" version = "1.15.0" @@ -1690,6 +2096,18 @@ files = [ {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, ] +[[package]] +name = "nanoid" +version = "2.0.0" +description = "A tiny, secure, URL-friendly, unique string ID generator for Python" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb"}, + {file = "nanoid-2.0.0.tar.gz", hash = "sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68"}, +] + [[package]] name = "ndindex" version = "1.10.0" @@ -2680,6 +3098,114 @@ mmh3 = "*" redis = ">=4.2.0rc1" typing_extensions = "*" +[[package]] +name = "propcache" +version = "0.3.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"}, + {file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"}, + {file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, + {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, + {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"}, + {file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"}, + {file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"}, + {file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"}, + {file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"}, + {file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"}, + {file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"}, + {file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"}, + {file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"}, + {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, + {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, +] + [[package]] name = "psutil" version = "7.0.0" @@ -3959,6 +4485,125 @@ xtgeo = "*" docs = ["autoapi", "myst-parser", "sphinx", "sphinx-argparse", "sphinx-autodoc-typehints (<2.4)", "sphinx-copybutton", "sphinx-togglebutton", "sphinx_rtd_theme", "sphinxcontrib-apidoc"] tests = ["coverage (>=4.1)", "mypy", "pylint", "pytest", "pytest-cov", "pytest-mock", "pytest-runner", "pytest-xdist", "rstcheck", "ruff", "types-PyYAML"] +[[package]] +name = "yarl" +version = "1.20.0" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f1f6670b9ae3daedb325fa55fbe31c22c8228f6e0b513772c2e1c623caa6ab22"}, + {file = "yarl-1.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85a231fa250dfa3308f3c7896cc007a47bc76e9e8e8595c20b7426cac4884c62"}, + {file = "yarl-1.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a06701b647c9939d7019acdfa7ebbfbb78ba6aa05985bb195ad716ea759a569"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7595498d085becc8fb9203aa314b136ab0516c7abd97e7d74f7bb4eb95042abe"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af5607159085dcdb055d5678fc2d34949bd75ae6ea6b4381e784bbab1c3aa195"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95b50910e496567434cb77a577493c26bce0f31c8a305135f3bda6a2483b8e10"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b594113a301ad537766b4e16a5a6750fcbb1497dcc1bc8a4daae889e6402a634"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:083ce0393ea173cd37834eb84df15b6853b555d20c52703e21fbababa8c129d2"}, + {file = "yarl-1.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f1a350a652bbbe12f666109fbddfdf049b3ff43696d18c9ab1531fbba1c977a"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fb0caeac4a164aadce342f1597297ec0ce261ec4532bbc5a9ca8da5622f53867"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d88cc43e923f324203f6ec14434fa33b85c06d18d59c167a0637164863b8e995"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e52d6ed9ea8fd3abf4031325dc714aed5afcbfa19ee4a89898d663c9976eb487"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ce360ae48a5e9961d0c730cf891d40698a82804e85f6e74658fb175207a77cb2"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:06d06c9d5b5bc3eb56542ceeba6658d31f54cf401e8468512447834856fb0e61"}, + {file = "yarl-1.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c27d98f4e5c4060582f44e58309c1e55134880558f1add7a87c1bc36ecfade19"}, + {file = "yarl-1.20.0-cp310-cp310-win32.whl", hash = "sha256:f4d3fa9b9f013f7050326e165c3279e22850d02ae544ace285674cb6174b5d6d"}, + {file = "yarl-1.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc906b636239631d42eb8a07df8359905da02704a868983265603887ed68c076"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fdb5204d17cb32b2de2d1e21c7461cabfacf17f3645e4b9039f210c5d3378bf3"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eaddd7804d8e77d67c28d154ae5fab203163bd0998769569861258e525039d2a"}, + {file = "yarl-1.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:634b7ba6b4a85cf67e9df7c13a7fb2e44fa37b5d34501038d174a63eaac25ee2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d409e321e4addf7d97ee84162538c7258e53792eb7c6defd0c33647d754172e"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ea52f7328a36960ba3231c6677380fa67811b414798a6e071c7085c57b6d20a9"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8703517b924463994c344dcdf99a2d5ce9eca2b6882bb640aa555fb5efc706a"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:077989b09ffd2f48fb2d8f6a86c5fef02f63ffe6b1dd4824c76de7bb01e4f2e2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0acfaf1da020253f3533526e8b7dd212838fdc4109959a2c53cafc6db611bff2"}, + {file = "yarl-1.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4230ac0b97ec5eeb91d96b324d66060a43fd0d2a9b603e3327ed65f084e41f8"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6a1e6ae21cdd84011c24c78d7a126425148b24d437b5702328e4ba640a8902"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:86de313371ec04dd2531f30bc41a5a1a96f25a02823558ee0f2af0beaa7ca791"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dd59c9dd58ae16eaa0f48c3d0cbe6be8ab4dc7247c3ff7db678edecbaf59327f"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a0bc5e05f457b7c1994cc29e83b58f540b76234ba6b9648a4971ddc7f6aa52da"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c9471ca18e6aeb0e03276b5e9b27b14a54c052d370a9c0c04a68cefbd1455eb4"}, + {file = "yarl-1.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40ed574b4df723583a26c04b298b283ff171bcc387bc34c2683235e2487a65a5"}, + {file = "yarl-1.20.0-cp311-cp311-win32.whl", hash = "sha256:db243357c6c2bf3cd7e17080034ade668d54ce304d820c2a58514a4e51d0cfd6"}, + {file = "yarl-1.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8c12cd754d9dbd14204c328915e23b0c361b88f3cffd124129955e60a4fbfcfb"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e"}, + {file = "yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018"}, + {file = "yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1"}, + {file = "yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b"}, + {file = "yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64"}, + {file = "yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3"}, + {file = "yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0"}, + {file = "yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e"}, + {file = "yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384"}, + {file = "yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62"}, + {file = "yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d"}, + {file = "yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5"}, + {file = "yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd"}, + {file = "yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f"}, + {file = "yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac"}, + {file = "yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:119bca25e63a7725b0c9d20ac67ca6d98fa40e5a894bd5d4686010ff73397914"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35d20fb919546995f1d8c9e41f485febd266f60e55383090010f272aca93edcc"}, + {file = "yarl-1.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:484e7a08f72683c0f160270566b4395ea5412b4359772b98659921411d32ad26"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d8a3d54a090e0fff5837cd3cc305dd8a07d3435a088ddb1f65e33b322f66a94"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0cf05ae2d3d87a8c9022f3885ac6dea2b751aefd66a4f200e408a61ae9b7f0d"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a884b8974729e3899d9287df46f015ce53f7282d8d3340fa0ed57536b440621c"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f8d8aa8dd89ffb9a831fedbcb27d00ffd9f4842107d52dc9d57e64cb34073d5c"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4e88d6c3c8672f45a30867817e4537df1bbc6f882a91581faf1f6d9f0f1b5a"}, + {file = "yarl-1.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdb77efde644d6f1ad27be8a5d67c10b7f769804fff7a966ccb1da5a4de4b656"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4ba5e59f14bfe8d261a654278a0f6364feef64a794bd456a8c9e823071e5061c"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:d0bf955b96ea44ad914bc792c26a0edcd71b4668b93cbcd60f5b0aeaaed06c64"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:27359776bc359ee6eaefe40cb19060238f31228799e43ebd3884e9c589e63b20"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d9c7a1dc0a26efb33e1acb56c8849bd57a693b85f44774356c92d610369efa"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:faa709b66ae0e24c8e5134033187a972d849d87ed0a12a0366bedcc6b5dc14a5"}, + {file = "yarl-1.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44869ee8538208fe5d9342ed62c11cc6a7a1af1b3d0bb79bb795101b6e77f6e0"}, + {file = "yarl-1.20.0-cp39-cp39-win32.whl", hash = "sha256:b7fa0cb9fd27ffb1211cde944b41f5c67ab1c13a13ebafe470b1e206b8459da8"}, + {file = "yarl-1.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:d4fad6e5189c847820288286732075f213eabf81be4d08d6cc309912e62be5b7"}, + {file = "yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124"}, + {file = "yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + [[package]] name = "zipp" version = "3.23.0" @@ -3982,4 +4627,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "ab9cb5225818d329efc08b1be7cd5e2e15ce884edb035d073112bd5199c38ceb" +content-hash = "ae5bce07aa5a9c4f3ecee96e22e1d1eb49f28d07e10b9c615187e23f922eb0db" diff --git a/backend_py/primary/primary/auth/enforce_logged_in_middleware.py b/backend_py/primary/primary/auth/enforce_logged_in_middleware.py index 4ecc4503a..9062b8fc7 100644 --- a/backend_py/primary/primary/auth/enforce_logged_in_middleware.py +++ b/backend_py/primary/primary/auth/enforce_logged_in_middleware.py @@ -128,8 +128,10 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) - path_is_protected = True - if path_to_check in ["/login", "/auth-callback"] + self._unprotected_paths: - path_is_protected = False + for unprotected in ["/login", "/auth-callback"] + self._unprotected_paths: + if path_to_check.startswith(unprotected): + path_is_protected = False + break if path_is_protected: diff --git a/backend_py/primary/primary/config.py b/backend_py/primary/primary/config.py index 14c3d9df8..510227a2e 100644 --- a/backend_py/primary/primary/config.py +++ b/backend_py/primary/primary/config.py @@ -33,3 +33,11 @@ DEFAULT_STALE_WHILE_REVALIDATE = 3600 * 24 # 24 hour REDIS_USER_SESSION_URL = "redis://redis-user-session:6379" REDIS_CACHE_URL = "redis://redis-cache:6379" + +COSMOS_DB_PROD_CONNECTION_STRING = os.environ.get("WEBVIZ_DB_CONNECTION_STRING", None) +# pylint: disable=line-too-long +COSMOS_DB_EMULATOR_URI = "https://host.docker.internal:8081/" +COSMOS_DB_EMULATOR_KEY = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;" + +PERSISTENCE_DB_NAME = "persistence" +DASHBOARDS_CONTAINER_NAME = "dashboards" diff --git a/backend_py/primary/primary/main.py b/backend_py/primary/primary/main.py index 0132dd6b1..1126ee736 100644 --- a/backend_py/primary/primary/main.py +++ b/backend_py/primary/primary/main.py @@ -12,6 +12,7 @@ from primary.auth.auth_helper import AuthHelper from primary.auth.enforce_logged_in_middleware import EnforceLoggedInMiddleware from primary.middleware.add_process_time_to_server_timing_middleware import AddProcessTimeToServerTimingMiddleware +from primary.services.database_access.setup_local_database import maybe_setup_local_database from primary.middleware.add_browser_cache import AddBrowserCacheMiddleware from primary.routers.dev.router import router as dev_router @@ -32,6 +33,9 @@ from primary.routers.vfp.router import router as vfp_router from primary.routers.well.router import router as well_router from primary.routers.well_completions.router import router as well_completions_router +from primary.routers.persistence.sessions.router import router as sessions_router +from primary.routers.persistence.snapshots.router import router as snapshots_router +from primary.routers.persistence.snapshot_preview.router import router as snapshot_preview_router from primary.services.utils.httpx_async_client_wrapper import HTTPX_ASYNC_CLIENT_WRAPPER from primary.utils.azure_monitor_setup import setup_azure_monitor_telemetry from primary.utils.exception_handlers import configure_service_level_exception_handlers @@ -58,6 +62,9 @@ LOGGER = logging.getLogger(__name__) +# Setup Cosmos DB emulator database if running locally +maybe_setup_local_database() + def custom_generate_unique_id(route: APIRoute) -> str: return f"{route.name}" @@ -106,6 +113,9 @@ async def shutdown_event_async() -> None: app.include_router(rft_router, prefix="/rft", tags=["rft"]) app.include_router(vfp_router, prefix="/vfp", tags=["vfp"]) app.include_router(dev_router, prefix="/dev", tags=["dev"], include_in_schema=False) +app.include_router(sessions_router, prefix="/sessions", tags=["sessions"]) +app.include_router(snapshots_router, prefix="/snapshots", tags=["snapshots"]) +app.include_router(snapshot_preview_router, prefix="/snapshot-preview", tags=["snapshot_preview"]) auth_helper = AuthHelper() app.include_router(auth_helper.router) @@ -120,7 +130,7 @@ async def shutdown_event_async() -> None: # Add out custom middleware to enforce that user is logged in # Also redirects to /login endpoint for some select paths -unprotected_paths = ["/logout", "/logged_in_user", "/alive", "/openapi.json"] +unprotected_paths = ["/logout", "/logged_in_user", "/alive", "/openapi.json", "/snapshot-preview"] paths_redirected_to_login = ["/", "/alive_protected"] app.add_middleware( diff --git a/backend_py/primary/primary/routers/persistence/sessions/converters.py b/backend_py/primary/primary/routers/persistence/sessions/converters.py new file mode 100644 index 000000000..0d208ef23 --- /dev/null +++ b/backend_py/primary/primary/routers/persistence/sessions/converters.py @@ -0,0 +1,34 @@ +from primary.services.database_access.session_access.model import SessionDocument +from primary.services.database_access.session_access.types import SessionMetadata, SessionMetadataWithId +from . import schemas + + +def to_api_session_metadata_summary(metadata: SessionMetadataWithId) -> schemas.SessionMetadataWithId: + return schemas.SessionMetadataWithId( + id=metadata.id, + title=metadata.title, + description=metadata.description, + createdAt=metadata.created_at.isoformat(), + updatedAt=metadata.updated_at.isoformat(), + version=metadata.version, + ) + + +def to_api_session_metadata(metadata: SessionMetadata) -> schemas.SessionMetadata: + return schemas.SessionMetadata( + title=metadata.title, + description=metadata.description, + createdAt=metadata.created_at.isoformat(), + updatedAt=metadata.updated_at.isoformat(), + version=metadata.version, + hash=metadata.hash, + ) + + +def to_api_session_record(document: SessionDocument) -> schemas.SessionDocument: + return schemas.SessionDocument( + id=document.id, + ownerId=document.owner_id, + metadata=to_api_session_metadata(document.metadata), + content=document.content, + ) diff --git a/backend_py/primary/primary/routers/persistence/sessions/router.py b/backend_py/primary/primary/routers/persistence/sessions/router.py new file mode 100644 index 000000000..27f2ca957 --- /dev/null +++ b/backend_py/primary/primary/routers/persistence/sessions/router.py @@ -0,0 +1,88 @@ +import logging +from typing import List, Optional + +from fastapi import APIRouter, Depends, HTTPException, Query + +from primary.middleware.add_browser_cache import no_cache +from primary.services.database_access.session_access.session_access import SessionAccess +from primary.auth.auth_helper import AuthHelper, AuthenticatedUser +from primary.services.database_access.session_access.types import ( + NewSession, + SessionUpdate, + SortBy, + SortDirection, +) +from primary.routers.persistence.sessions.converters import ( + to_api_session_metadata_summary, + to_api_session_metadata, + to_api_session_record, +) + +from . import schemas + +LOGGER = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/sessions", response_model=List[schemas.SessionMetadataWithId]) +@no_cache +async def get_sessions_metadata( + user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), + sort_by: Optional[SortBy] = Query(None, description="Sort the result by"), + sort_direction: Optional[SortDirection] = Query(SortDirection.ASC, description="Sort direction: 'asc' or 'desc'"), + limit: Optional[int] = Query(10, ge=1, le=100, description="Limit the number of results"), +): + access = SessionAccess.create(user.get_user_id()) + async with access: + items = await access.get_filtered_sessions_metadata_for_user_async( + sort_by=sort_by, sort_direction=sort_direction, limit=limit + ) + return [to_api_session_metadata_summary(item) for item in items] + + +@router.get("/sessions/{session_id}", response_model=schemas.SessionDocument) +@no_cache +async def get_session(session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)): + access = SessionAccess.create(user.get_user_id()) + async with access: + session = await access.get_session_by_id_async(session_id) + if not session: + raise HTTPException(status_code=404, detail="Session not found") + return to_api_session_record(session) + + +@router.get("/sessions/metadata/{session_id}", response_model=schemas.SessionMetadata) +@no_cache +async def get_session_metadata(session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)): + access = SessionAccess.create(user.get_user_id()) + async with access: + metadata = await access.get_session_metadata_async(session_id) + if not metadata: + raise HTTPException(status_code=404, detail="Session metadata not found") + return to_api_session_metadata(metadata) + + +@router.post("/sessions", response_model=str) +async def create_session(session: NewSession, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)): + access = SessionAccess.create(user.get_user_id()) + async with access: + session_id = await access.insert_session_async(session) + return session_id + + +@router.put("/sessions/{session_id}") +async def update_session( + session_id: str, + session_update: SessionUpdate, + user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), +): + access = SessionAccess.create(user.get_user_id()) + async with access: + await access.update_session_async(session_id, session_update) + + +@router.delete("/sessions/{session_id}") +async def delete_session(session_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)): + access = SessionAccess.create(user.get_user_id()) + async with access: + await access.delete_session_async(session_id) diff --git a/backend_py/primary/primary/routers/persistence/sessions/schemas.py b/backend_py/primary/primary/routers/persistence/sessions/schemas.py new file mode 100644 index 000000000..a79b90f24 --- /dev/null +++ b/backend_py/primary/primary/routers/persistence/sessions/schemas.py @@ -0,0 +1,27 @@ +from typing import Optional +from pydantic import BaseModel + + +class SessionMetadataWithId(BaseModel): + id: str + title: str + description: Optional[str] + createdAt: str + updatedAt: str + version: int + + +class SessionMetadata(BaseModel): + title: str + description: Optional[str] + createdAt: str + updatedAt: str + version: int + hash: str + + +class SessionDocument(BaseModel): + id: str + ownerId: str + metadata: SessionMetadata + content: str diff --git a/backend_py/primary/primary/routers/persistence/snapshot_preview/router.py b/backend_py/primary/primary/routers/persistence/snapshot_preview/router.py new file mode 100644 index 000000000..68cacc2c1 --- /dev/null +++ b/backend_py/primary/primary/routers/persistence/snapshot_preview/router.py @@ -0,0 +1,44 @@ +import html +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import HTMLResponse + +from primary.services.database_access.snapshot_access.snapshot_access import SnapshotAccess + +router = APIRouter() + + +@router.get("/{snapshot_id}", response_class=HTMLResponse) +async def snapshot_preview(snapshot_id: str, request: Request): + access = await SnapshotAccess.create("") + async with access: + metadata = await access.get_snapshot_metadata(snapshot_id) + if not metadata: + raise HTTPException(status_code=404, detail="Snapshot metadata not found") + + base_url = get_external_base_url(request) + snapshot_url = f"{base_url}/snapshot/{snapshot_id}" + + title = html.escape(metadata.title) + description = html.escape(metadata.description or "No description available") + + return f""" + + +
+ + + + + + + + Redirecting… + + + """ + + +def get_external_base_url(request: Request) -> str: + forwarded_proto = request.headers.get("x-forwarded-proto", "http") + forwarded_host = request.headers.get("x-forwarded-host", request.headers.get("host", "localhost")) + return f"{forwarded_proto}://{forwarded_host}" diff --git a/backend_py/primary/primary/routers/persistence/snapshots/converters.py b/backend_py/primary/primary/routers/persistence/snapshots/converters.py new file mode 100644 index 000000000..cba984578 --- /dev/null +++ b/backend_py/primary/primary/routers/persistence/snapshots/converters.py @@ -0,0 +1,46 @@ +from primary.services.database_access.snapshot_access.models import SnapshotAccessLog +from primary.services.database_access.snapshot_access.types import Snapshot, SnapshotMetadata, SnapshotMetadataWithId + +from . import schemas + + +def to_api_snapshot_metadata_summary(metadata: SnapshotMetadataWithId) -> schemas.SnapshotMetadataWithId: + return schemas.SnapshotMetadataWithId( + id=metadata.id, + ownerId=metadata.owner_id, + title=metadata.title, + description=metadata.description, + createdAt=metadata.created_at.isoformat(), + updatedAt=metadata.updated_at.isoformat(), + hash=metadata.hash, + ) + + +def to_api_snapshot_metadata(metadata: SnapshotMetadata) -> schemas.SnapshotMetadata: + return schemas.SnapshotMetadata( + ownerId=metadata.owner_id, + title=metadata.title, + description=metadata.description, + createdAt=metadata.created_at.isoformat(), + updatedAt=metadata.updated_at.isoformat(), + hash=metadata.hash, + ) + + +def to_api_snapshot(snapshot: Snapshot) -> schemas.Snapshot: + return schemas.Snapshot( + id=snapshot.id, + metadata=to_api_snapshot_metadata(snapshot.metadata), + content=snapshot.content, + ) + + +def to_api_snapshot_access_log(access_log: SnapshotAccessLog, metadata: SnapshotMetadata) -> schemas.SnapshotAccessLog: + return schemas.SnapshotAccessLog( + visitorId=access_log.visitor_id, + snapshotId=access_log.snapshot_id, + visits=access_log.visits, + firstVisitedAt=access_log.first_visited_at.isoformat() if access_log.first_visited_at else None, + lastVisitedAt=access_log.last_visited_at.isoformat() if access_log.last_visited_at else None, + snapshotMetadata=to_api_snapshot_metadata(metadata), + ) diff --git a/backend_py/primary/primary/routers/persistence/snapshots/router.py b/backend_py/primary/primary/routers/persistence/snapshots/router.py new file mode 100644 index 000000000..961ff27cb --- /dev/null +++ b/backend_py/primary/primary/routers/persistence/snapshots/router.py @@ -0,0 +1,134 @@ +import logging +from typing import List, Optional + +from fastapi import APIRouter, Depends, HTTPException, Query + +from primary.services.database_access.snapshot_access.types import ( + NewSnapshot, + SnapshotUpdate, + SortBy, + SortDirection, +) +from primary.middleware.add_browser_cache import no_cache +from primary.services.database_access.snapshot_access.snapshot_access import SnapshotAccess +from primary.services.database_access.snapshot_access.snapshot_logs_access import SnapshotLogsAccess +from primary.services.database_access.snapshot_access.query_collation_options import QueryCollationOptions + + +from primary.auth.auth_helper import AuthHelper, AuthenticatedUser +from primary.routers.persistence.snapshots.converters import ( + to_api_snapshot, + to_api_snapshot_access_log, + to_api_snapshot_metadata, + to_api_snapshot_metadata_summary, +) + + +from . import schemas + +LOGGER = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("/recent_snapshots", response_model=list[schemas.SnapshotAccessLog]) +async def get_recent_snapshots( + user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), + sort_by: Optional[SortBy] = Query(SortBy.LAST_VISIT, description="Sort the result by"), + sort_direction: Optional[SortDirection] = Query(SortDirection.DESC, description="Sort direction: 'asc' or 'desc'"), + limit: Optional[int] = Query(5, ge=1, le=100, description="Limit the number of results"), + offset: Optional[int] = Query(0, ge=0, description="The offset of the results"), +) -> list[schemas.SnapshotAccessLog]: + async with ( + SnapshotAccess.create(user.get_user_id()) as snapshot_access, + SnapshotLogsAccess.create(user.get_user_id()) as log_access, + ): + collation_options = QueryCollationOptions(sort_by=sort_by, sort_dir=sort_direction, limit=limit, offset=offset) + + recent_logs = await log_access.get_access_logs_for_user_async(collation_options) + + payload: list[schemas.SnapshotAccessLog] = [] + + for log in recent_logs: + metadata = await snapshot_access.get_snapshot_metadata_async(log.snapshot_id, log.snapshot_owner_id) + + payload.append(to_api_snapshot_access_log(log, metadata)) + + return payload + + +@router.get("/snapshots", response_model=List[schemas.SnapshotMetadata]) +@no_cache +async def get_snapshots_metadata( + user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), + sort_by: Optional[SortBy] = Query(SortBy.LAST_VISIT, description="Sort the result by"), + sort_direction: Optional[SortDirection] = Query(SortDirection.DESC, description="Sort direction: 'asc' or 'desc'"), + limit: Optional[int] = Query(10, ge=1, le=100, description="Limit the number of results"), +) -> List[schemas.SnapshotMetadata]: + access = SnapshotAccess.create(user.get_user_id()) + async with access: + items = await access.get_filtered_snapshots_metadata_for_user_async( + sort_by=sort_by, sort_direction=sort_direction, limit=limit + ) + return [to_api_snapshot_metadata_summary(item) for item in items] + + +@router.get("/snapshots/{snapshot_id}", response_model=schemas.Snapshot) +@no_cache +async def get_snapshot( + snapshot_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user) +) -> schemas.Snapshot: + access = SnapshotAccess.create(user.get_user_id()) + logs_access = SnapshotLogsAccess.create(user_id=user.get_user_id()) + + async with access, logs_access: + snapshot = await access.get_snapshot_by_id_async(snapshot_id) + if not snapshot: + raise HTTPException(status_code=404, detail="Snapshot not found") + + await logs_access.log_snapshot_visit_async(snapshot_id, snapshot.owner_id) + + return to_api_snapshot(snapshot) + + +@router.get("/snapshots/metadata/{snapshot_id}", response_model=schemas.SnapshotMetadata) +@no_cache +async def get_snapshot_metadata(snapshot_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)): + access = SnapshotAccess.create(user.get_user_id()) + async with access: + metadata = await access.get_snapshot_metadata_async(snapshot_id) + if not metadata: + raise HTTPException(status_code=404, detail="Session metadata not found") + return to_api_snapshot_metadata(metadata) + + +@router.post("/snapshots", response_model=str) +async def create_snapshot( + session: NewSnapshot, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user) +) -> str: + access = SnapshotAccess.create(user.get_user_id()) + logs_access = SnapshotLogsAccess.create(user.get_user_id()) + + async with access, logs_access: + snapshot_id = await access.insert_snapshot_async(session) + + # We count snapshot creation as implicit visit. This also makes it so + await logs_access.log_snapshot_visit_async(snapshot_id=snapshot_id, snapshot_owner_id=user.get_user_id()) + return snapshot_id + + +@router.put("/snapshots/{snapshot_id}") +async def update_snapshot( + snapshot_id: str, + snapshot_update: SnapshotUpdate, + user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user), +): + access = SnapshotAccess.create(user.get_user_id()) + async with access: + await access.update_snapshot_metadata_async(snapshot_id, snapshot_update) + + +@router.delete("/snapshots/{snapshot_id}") +async def delete_snapshot(snapshot_id: str, user: AuthenticatedUser = Depends(AuthHelper.get_authenticated_user)): + access = SnapshotAccess.create(user.get_user_id()) + async with access: + await access.delete_snapshot_async(snapshot_id) diff --git a/backend_py/primary/primary/routers/persistence/snapshots/schemas.py b/backend_py/primary/primary/routers/persistence/snapshots/schemas.py new file mode 100644 index 000000000..0b2469cbc --- /dev/null +++ b/backend_py/primary/primary/routers/persistence/snapshots/schemas.py @@ -0,0 +1,40 @@ +from typing import Optional +from pydantic import BaseModel + +from primary.services.database_access.snapshot_access.util import make_access_log_item_id + + +class SnapshotMetadata(BaseModel): + ownerId: str + title: str + description: Optional[str] + createdAt: str + updatedAt: str + hash: str + + +class SnapshotMetadataWithId(SnapshotMetadata): + id: str + + +class Snapshot(BaseModel): + id: str + metadata: SnapshotMetadata + content: str + + +class SnapshotAccessLog(BaseModel): + visitorId: str + snapshotId: str + visits: int + firstVisitedAt: str | None + lastVisitedAt: str | None + + snapshotMetadata: SnapshotMetadata + + # Internal item id + @property + # pylint: disable=invalid-name + # ↳ pylint v2 will complain about names that are shorter than 3 characters + def id(self) -> str: + return make_access_log_item_id(self.snapshotId, self.visitorId) diff --git a/backend_py/primary/primary/services/database_access/__init__.py b/backend_py/primary/primary/services/database_access/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend_py/primary/primary/services/database_access/_utils.py b/backend_py/primary/primary/services/database_access/_utils.py new file mode 100644 index 000000000..95f04a475 --- /dev/null +++ b/backend_py/primary/primary/services/database_access/_utils.py @@ -0,0 +1,13 @@ +import hashlib + + +# Utility function to hash a JSON string using SHA-256 +# This function mimics the behavior of TextEncoder in JavaScript, which encodes strings to +# UTF-8 before hashing. The output is a hexadecimal string representation of the hash. +# +# It is important that this function returns the same hash as the JavaScript version +def hash_json_string(json_string: str) -> str: + data = json_string.encode("utf-8") # Matches TextEncoder behavior + hash_bytes = hashlib.sha256(data).digest() + hash_hex = "".join(f"{b:02x}" for b in hash_bytes) + return hash_hex diff --git a/backend_py/primary/primary/services/database_access/container_access.py b/backend_py/primary/primary/services/database_access/container_access.py new file mode 100644 index 000000000..4ac2b65fd --- /dev/null +++ b/backend_py/primary/primary/services/database_access/container_access.py @@ -0,0 +1,118 @@ +import logging +from typing import Dict, Generic, List, Optional, Type, TypeVar +from azure.cosmos.aio import ContainerProxy +from azure.cosmos import exceptions +from pydantic import BaseModel, ValidationError + +from primary.services.service_exceptions import Service, ServiceRequestError +from .database_access import DatabaseAccess + + +logger = logging.getLogger(__name__) + +T = TypeVar("T", bound=BaseModel) + +""" +ContainerAccess provides access to a specific container in a Cosmos DB database. +It allows for querying, inserting, updating, and deleting items in the container. +It uses a Pydantic model for item validation and serialization. + +It is designed to be used with asynchronous context management, ensuring proper resource cleanup. +It raises ServiceRequestError for any issues encountered during operations, providing a clear error message. +""" + + +class ContainerAccess(Generic[T]): + def __init__( + self, + database_name: str, + container_name: str, + database_access: DatabaseAccess, + container: ContainerProxy, + validation_model: Type[T], + ): + self._database_name = database_name + self._container_name = container_name + self._database_access = database_access + self._container = container + self._validation_model: Type[T] = validation_model + + @classmethod + def create(cls, database_name: str, container_name: str, validation_model: Type[T]) -> "ContainerAccess[T]": + """Create a ContainerAccess instance.""" + db_access = DatabaseAccess.create(database_name) + container = db_access.get_container(container_name) + logger.debug("[ContainerAccess] Created for container '%s' in database '%s'", container_name, database_name) + return cls(database_name, container_name, db_access, container, validation_model) + + async def __aenter__(self): # pylint: disable=C9001 + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): # pylint: disable=C9001 + await self.close_async() + + def _raise_exception(self, message: str): + raise ServiceRequestError( + f"ContainerAccess ({self._database_name}, {self._container_name}): {message}", Service.DATABASE + ) + + async def query_items_async(self, query: str, parameters: Optional[List[Dict[str, object]]] = None) -> List[T]: + try: + items_iterable = self._container.query_items( + query=query, + parameters=parameters or [], + ) + items = [item async for item in items_iterable] + return [self._validation_model.model_validate(item) for item in items] + except ValidationError as validation_error: + logger.error("[ContainerAccess] Validation error in '%s': %s", self._container_name, validation_error) + raise + except exceptions.CosmosHttpResponseError as error: + self._raise_exception(error.message) + + async def get_item_async(self, item_id: str, partition_key: str) -> T: + try: + item = await self._container.read_item(item=item_id, partition_key=partition_key) + return self._validation_model.model_validate(item) + except ValidationError as validation_error: + logger.error("[ContainerAccess] Validation error in '%s': %s", self._container_name, validation_error) + raise + except exceptions.CosmosHttpResponseError as error: + self._raise_exception(error.message) + + async def insert_item_async(self, item: T) -> str: + try: + item = self._validation_model.model_validate(item).model_dump(by_alias=True, mode="json") + result = await self._container.upsert_item(item) + return result["id"] + except ValidationError as validation_error: + logger.error("[ContainerAccess] Validation error in '%s': %s", self._container_name, validation_error) + raise + except exceptions.CosmosHttpResponseError as error: + self._raise_exception(error.message) + + async def delete_item_async(self, item_id: str, partition_key: str): + try: + await self._container.delete_item(item=item_id, partition_key=partition_key) + logger.debug("[ContainerAccess] Deleted item '%s' from '%s'", item_id, self._container_name) + except exceptions.CosmosHttpResponseError as error: + self._raise_exception(error.message) + + async def update_item_async(self, item_id: str, updated_item: T): + try: + validated = self._validation_model.model_validate(updated_item).model_dump(by_alias=True, mode="json") + await self._container.upsert_item(validated) + logger.debug("[ContainerAccess] Updated item '%s' in '%s'", item_id, self._container_name) + except ValidationError as validation_error: + logger.error("[ContainerAccess] Validation error in '%s': %s", self._container_name, validation_error) + raise + except exceptions.CosmosHttpResponseError as error: + self._raise_exception(error.message) + + async def close_async(self): + """Close the container access.""" + if self._database_access: + logger.debug("[ContainerAccess] Closing access to '%s/%s'", self._database_name, self._container_name) + await self._database_access.close_async() + self._database_access = None + self._container = None diff --git a/backend_py/primary/primary/services/database_access/database_access.py b/backend_py/primary/primary/services/database_access/database_access.py new file mode 100644 index 000000000..b8bc9f369 --- /dev/null +++ b/backend_py/primary/primary/services/database_access/database_access.py @@ -0,0 +1,54 @@ +from azure.cosmos.aio import CosmosClient, ContainerProxy +from azure.cosmos import exceptions + +from primary.config import COSMOS_DB_PROD_CONNECTION_STRING, COSMOS_DB_EMULATOR_URI, COSMOS_DB_EMULATOR_KEY +from primary.services.service_exceptions import Service, ServiceRequestError + + +class DatabaseAccess: + def __init__(self, database_name: str, client: CosmosClient): + self._database_name = database_name + self._client = client + self._database = self._client.get_database_client(database_name) + + @classmethod + def create(cls, database_name: str) -> "DatabaseAccess": + if COSMOS_DB_PROD_CONNECTION_STRING: + client = CosmosClient.from_connection_string(COSMOS_DB_PROD_CONNECTION_STRING) + elif COSMOS_DB_EMULATOR_URI and COSMOS_DB_EMULATOR_KEY: + client = CosmosClient(COSMOS_DB_EMULATOR_URI, COSMOS_DB_EMULATOR_KEY, connection_verify=False) + else: + raise ServiceRequestError( + "No Cosmos DB production connection string or emulator URI/key provided.", Service.DATABASE + ) + self = cls(database_name, client) + return self + + async def __aenter__(self): # pylint: disable=C9001 + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): # pylint: disable=C9001 + await self.close_async() + + def _raise_exception(self, message: str): + raise ServiceRequestError(f"DatabaseAccess ({self._database_name}): {message}", Service.DATABASE) + + def get_container(self, container_name: str) -> ContainerProxy: + if not self._client or not self._database: + self._raise_exception("Database client is not initialized or already closed.") + if not container_name or not isinstance(container_name, str): + self._raise_exception("Invalid container name.") + + try: + container = self._database.get_container_client(container_name) + return container + except exceptions.CosmosHttpResponseError as error: + self._raise_exception(f"Unable to access container '{container_name}': {error.message}") + + return None # unreachable; satisfies pylint R1710 + + async def close_async(self): + if self._client: + await self._client.close() + self._client = None + self._database = None diff --git a/backend_py/primary/primary/services/database_access/session_access/model.py b/backend_py/primary/primary/services/database_access/session_access/model.py new file mode 100644 index 000000000..e29d2d809 --- /dev/null +++ b/backend_py/primary/primary/services/database_access/session_access/model.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel, ConfigDict, field_validator +from primary.services.database_access.session_access.types import SessionMetadata + + +class SessionDocument(BaseModel): + id: str + owner_id: str + metadata: SessionMetadata + content: str + + model_config = ConfigDict(extra="ignore") diff --git a/backend_py/primary/primary/services/database_access/session_access/session_access.py b/backend_py/primary/primary/services/database_access/session_access/session_access.py new file mode 100644 index 000000000..4870f42e2 --- /dev/null +++ b/backend_py/primary/primary/services/database_access/session_access/session_access.py @@ -0,0 +1,144 @@ +from typing import Optional, List +from datetime import datetime, timezone +from nanoid import generate +from azure.cosmos.exceptions import CosmosResourceNotFoundError + +from primary.services.database_access.session_access.model import SessionDocument +from primary.services.database_access._utils import hash_json_string +from primary.services.service_exceptions import Service, ServiceRequestError +from primary.services.database_access.container_access import ContainerAccess +from primary.services.database_access.session_access.types import ( + NewSession, + SessionMetadataWithId, + SessionMetadata, + SessionUpdate, + SortBy, + SortDirection, +) + + +class SessionAccess: + CONTAINER_NAME = "sessions" + DATABASE_NAME = "persistence" + + def __init__(self, user_id: str, session_container_access: ContainerAccess[SessionDocument]): + self.user_id = user_id + self.session_container_access = session_container_access + + async def __aenter__(self): # pylint: disable=C9001 + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): # pylint: disable=C9001 + await self.session_container_access.close_async() + + @classmethod + def create(cls, user_id: str): + session_container_access = ContainerAccess.create(cls.DATABASE_NAME, cls.CONTAINER_NAME, SessionDocument) + return cls(user_id=user_id, session_container_access=session_container_access) + + async def get_session_by_id_async(self, session_id: str) -> SessionDocument: + document = await self.session_container_access.get_item_async(item_id=session_id, partition_key=self.user_id) + return document + + async def get_all_sessions_metadata_for_user_async(self) -> List[SessionMetadataWithId]: + query = "SELECT * FROM c WHERE c.owner_id = @owner_id" + params = [{"name": "@owner_id", "value": self.user_id}] + items = await self.session_container_access.query_items_async(query=query, parameters=params) + return [self._to_metadata_summary(item) for item in items] + + async def get_filtered_sessions_metadata_for_user_async( + self, + sort_by: Optional[SortBy] = SortBy.CREATED_AT, + sort_direction: Optional[SortDirection] = SortDirection.ASC, + limit: Optional[int] = None, + offset: Optional[int] = 0, + ) -> List[SessionMetadataWithId]: + if not isinstance(sort_by.value, str) or not sort_by.value.isidentifier(): + raise ServiceRequestError("Invalid sort field specified.", Service.DATABASE) + + if sort_by == SortBy.TITLE_LOWER: + metadata_array = await self.get_all_sessions_metadata_for_user_async() + + reverse = sort_direction == SortDirection.DESC + metadata_array.sort(key=lambda s: s.title.lower() if s.title else "", reverse=reverse) + + return metadata_array[offset:] if limit is None else metadata_array[offset : offset + limit] + + offset_clause = f"OFFSET {offset} LIMIT {limit}" if limit is not None else "" + query = ( + f"SELECT * FROM c " + f"WHERE c.owner_id = @owner_id " + f"ORDER BY c.metadata.{sort_by.value} {sort_direction.value} " + offset_clause + ) + + params = [ + {"name": "@owner_id", "value": self.user_id}, + ] + + items = await self.session_container_access.query_items_async(query=query, parameters=params) + + return [self._to_metadata_summary(item) for item in items] + + async def get_session_metadata_async(self, session_id: str) -> SessionMetadata: + existing = await self._assert_ownership_async(session_id) + + return existing.metadata + + async def insert_session_async(self, new_session: NewSession) -> str: + now = datetime.now(timezone.utc) + session_id = str(generate(size=8)) # Generate a unique session ID + session = SessionDocument( + id=session_id, + owner_id=self.user_id, + metadata=SessionMetadata( + title=new_session.title, + description=new_session.description, + created_at=now, + updated_at=now, + version=1, + hash=hash_json_string(new_session.content), + ), + content=new_session.content, + ) + return await self.session_container_access.insert_item_async(session) + + async def delete_session_async(self, session_id: str): + await self._assert_ownership_async(session_id) + await self.session_container_access.delete_item_async(session_id, partition_key=self.user_id) + + async def update_session_async(self, session_id: str, session_update: SessionUpdate): + existing = await self._assert_ownership_async(session_id) + + updated_metadata = existing.metadata.model_copy( + update={ + "title": session_update.metadata.title, + "description": session_update.metadata.description, + "version": existing.metadata.version + 1, + "updated_at": datetime.now(timezone.utc), + "hash": hash_json_string(session_update.content), + } + ) + + updated_session = SessionDocument( + id=session_id, + owner_id=self.user_id, + content=session_update.content, + metadata=updated_metadata, + ) + + await self.session_container_access.update_item_async(session_id, updated_session) + + async def _assert_ownership_async(self, session_id: str) -> SessionDocument: + try: + session = await self.session_container_access.get_item_async(item_id=session_id, partition_key=self.user_id) + except CosmosResourceNotFoundError: + raise ServiceRequestError(f"Session with id '{session_id}' not found.", Service.DATABASE) + + if session.owner_id != self.user_id: + raise ServiceRequestError(f"You do not have permission to access session '{session_id}'.", Service.DATABASE) + + return session + + @staticmethod + def _to_metadata_summary(doc: SessionDocument) -> SessionMetadataWithId: + return SessionMetadataWithId(**doc.metadata.model_dump(), id=doc.id) diff --git a/backend_py/primary/primary/services/database_access/session_access/types.py b/backend_py/primary/primary/services/database_access/session_access/types.py new file mode 100644 index 000000000..4559374cb --- /dev/null +++ b/backend_py/primary/primary/services/database_access/session_access/types.py @@ -0,0 +1,49 @@ +from datetime import datetime +from enum import Enum +from typing import Optional + +from pydantic import BaseModel + + +class SessionUserEditableMetadata(BaseModel): + title: str + description: Optional[str] = None + + +class SessionMetadataInternal(BaseModel): + created_at: datetime + updated_at: datetime + hash: str + version: int + + +class SessionMetadata(SessionUserEditableMetadata, SessionMetadataInternal): + pass + + +class SessionMetadataWithId(SessionMetadata): + id: str + + +class SessionUpdate(BaseModel): + id: str + metadata: SessionUserEditableMetadata + content: str + + +class NewSession(BaseModel): + title: str + description: Optional[str] + content: str + + +class SortBy(str, Enum): + CREATED_AT = "created_at" + UPDATED_AT = "updated_at" + TITLE = "title" + TITLE_LOWER = "title_lower" + + +class SortDirection(str, Enum): + ASC = "asc" + DESC = "desc" diff --git a/backend_py/primary/primary/services/database_access/setup_local_database.py b/backend_py/primary/primary/services/database_access/setup_local_database.py new file mode 100644 index 000000000..338065382 --- /dev/null +++ b/backend_py/primary/primary/services/database_access/setup_local_database.py @@ -0,0 +1,108 @@ +""" +This file is only used for setting up the local database for development and testing purposes. +""" + +import logging +import time +from typing import Optional, List, Dict, Any +import ssl +import urllib.request +from urllib.error import URLError + +from azure.cosmos import CosmosClient, PartitionKey, DatabaseProxy + +from primary.config import COSMOS_DB_PROD_CONNECTION_STRING, COSMOS_DB_EMULATOR_URI, COSMOS_DB_EMULATOR_KEY + +LOGGER = logging.getLogger(__name__) + +# Declarative schema definition +COSMOS_SCHEMA: List[Dict[str, Any]] = [ + { + "database": "persistence", + "offer_throughput": 4000, + "containers": [ + {"id": "sessions", "partition_key": "/owner_id"}, + {"id": "snapshots_metadata", "partition_key": "/owner_id"}, + {"id": "snapshots_content", "partition_key": "/snapshot_id"}, + {"id": "snapshot_access_log", "partition_key": "/visitor_id"}, + ], + }, +] + + +def wait_for_emulator(uri: str, key: str, retries: int = 50, delay: int = 10) -> CosmosClient: + probe_url = f"{uri.rstrip('/')}/_explorer/emulator.pem" + context = ssl._create_unverified_context() + + for attempt in range(retries): + try: + with urllib.request.urlopen(probe_url, context=context) as response: + if response.status == 200: + LOGGER.info("✅ Emulator HTTPS endpoint is up. Proceeding to create CosmosClient.") + break + except URLError as e: + LOGGER.warning("⏳ Emulator cert endpoint not ready (attempt %d): %s", attempt + 1, e.reason) + time.sleep(delay) + else: + raise RuntimeError("❌ Cosmos Emulator certificate endpoint not ready after timeout") + + # Now that we know HTTPS works, create the CosmosClient + return CosmosClient(uri, key, connection_verify=False) + + +def create_database_with_retry(client: CosmosClient, db_def: Dict[str, Any], max_attempts: int = 5) -> DatabaseProxy: + db_name: str = db_def["database"] + for attempt in range(1, max_attempts + 1): + try: + return client.create_database_if_not_exists(db_name, offer_throughput=db_def.get("offer_throughput")) + except Exception as error: + LOGGER.warning("⚠️ Failed to create database '%s' (attempt %d): %s", db_name, attempt, error) + if attempt == max_attempts: + raise + time.sleep(2 * attempt) + + raise RuntimeError(f"Failed to create database '{db_name}' after {max_attempts} attempts.") + + +def maybe_setup_local_database() -> None: + if COSMOS_DB_PROD_CONNECTION_STRING: + LOGGER.info("Using production Cosmos DB - skipping local setup.") + return + + if COSMOS_DB_EMULATOR_URI is None or COSMOS_DB_EMULATOR_KEY is None: + raise ValueError("No Cosmos DB production connection string or emulator URI/key provided.") + + client: CosmosClient = wait_for_emulator(COSMOS_DB_EMULATOR_URI, COSMOS_DB_EMULATOR_KEY) + + total_containers = 0 + + for db_def in COSMOS_SCHEMA: + database: DatabaseProxy = create_database_with_retry(client, db_def) + + for container_def in db_def["containers"]: + max_attempts = 5 + for attempt in range(1, max_attempts + 1): + try: + database.create_container_if_not_exists( + id=container_def["id"], + partition_key=PartitionKey(path=container_def["partition_key"]), + offer_throughput=container_def.get("throughput"), + indexing_policy=container_def.get("indexing_policy"), + ) + LOGGER.info(" ✅ Created container '%s' (attempt %d)", container_def["id"], attempt) + break + except Exception as error: + LOGGER.warning( + " ⚠️ Failed to create container '%s' (attempt %d): %s", container_def["id"], attempt, error + ) + if attempt == max_attempts: + raise + time.sleep(2 * attempt) + + total_containers += 1 + + LOGGER.info( + "✅ Local Cosmos DB emulator setup complete: %d database(s), %d container(s).", + len(COSMOS_SCHEMA), + total_containers, + ) diff --git a/backend_py/primary/primary/services/database_access/snapshot_access/__init__.py b/backend_py/primary/primary/services/database_access/snapshot_access/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend_py/primary/primary/services/database_access/snapshot_access/models.py b/backend_py/primary/primary/services/database_access/snapshot_access/models.py new file mode 100644 index 000000000..aa4bb7038 --- /dev/null +++ b/backend_py/primary/primary/services/database_access/snapshot_access/models.py @@ -0,0 +1,59 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, ValidationInfo +from pydantic import computed_field, field_validator + + +from primary.services.database_access.snapshot_access.types import SnapshotMetadata + +from .util import make_access_log_item_id + + +class SnapshotMetadataDocument(BaseModel): + id: str + snapshot_id: str + owner_id: str + metadata: SnapshotMetadata + + @field_validator("snapshot_id") + @classmethod + def validate_snapshot_id(cls, val: str, info: ValidationInfo) -> str: + if val != info.data.get("id"): + raise ValueError("snapshot_id must equal id") + return val + + model_config = ConfigDict(extra="ignore") + + +class SnapshotContentDocument(BaseModel): + id: str + snapshot_id: str + owner_id: str + content: str + + @field_validator("snapshot_id") + @classmethod + def validate_snapshot_id(cls, val: str, info: ValidationInfo) -> str: + if val != info.data.get("id"): + raise ValueError("snapshot_id must equal id") + return val + + model_config = ConfigDict(extra="ignore") + + +class SnapshotAccessLog(BaseModel): + model_config = ConfigDict(extra="ignore") + + visitor_id: str # Partition key + snapshot_id: str + snapshot_owner_id: str + visits: int = 0 + first_visited_at: datetime | None = None + last_visited_at: datetime | None = None + + # Internal item id + @computed_field # type: ignore[prop-decorator] + @property + # pylint: disable=invalid-name + # ↳ pylint v2 will complain about names that are shorter than 3 characters + def id(self) -> str: + return make_access_log_item_id(self.snapshot_id, self.visitor_id) diff --git a/backend_py/primary/primary/services/database_access/snapshot_access/query_collation_options.py b/backend_py/primary/primary/services/database_access/snapshot_access/query_collation_options.py new file mode 100644 index 000000000..cc5052e5f --- /dev/null +++ b/backend_py/primary/primary/services/database_access/snapshot_access/query_collation_options.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +from .types import SortBy, SortDirection + + +# TODO: Generalize utility to work with any model +@dataclass +class QueryCollationOptions: + """Helper class for defining NoSQL collation options""" + + sort_by: SortBy | None = None + sort_dir: SortDirection | None = None # "asc" or "desc" + limit: int | None = None + offset: int | None = 0 + + def to_sql_query_string(self, variable_name: str = "c") -> str | None: + tokens = [] + + if self.sort_by: + tokens.append(f"ORDER BY {variable_name}.{self.sort_by.value}") + + if self.sort_dir: + tokens.append(self.sort_dir.value) + + # Zero is arguably a valid limit, so explicitly check None + if self.limit is not None: + tokens.append(f"OFFSET {self.offset} LIMIT {self.limit}") + + if tokens: + return " ".join(tokens) + + return None diff --git a/backend_py/primary/primary/services/database_access/snapshot_access/snapshot_access.py b/backend_py/primary/primary/services/database_access/snapshot_access/snapshot_access.py new file mode 100644 index 000000000..67c9dd14c --- /dev/null +++ b/backend_py/primary/primary/services/database_access/snapshot_access/snapshot_access.py @@ -0,0 +1,205 @@ +from typing import Optional, List +from datetime import datetime, timezone +from nanoid import generate +from azure.cosmos.exceptions import CosmosResourceNotFoundError + +from primary.services.database_access.snapshot_access.models import SnapshotContentDocument, SnapshotMetadataDocument +from primary.services.database_access._utils import hash_json_string +from primary.services.service_exceptions import Service, ServiceRequestError +from primary.services.database_access.container_access import ContainerAccess +from primary.services.database_access.snapshot_access.types import ( + NewSnapshot, + SnapshotMetadata, + SnapshotMetadataWithId, + Snapshot, + SnapshotUpdate, + SortBy, + SortDirection, +) + + +class SnapshotAccess: + CONTAINER_NAMES = { + "content": "snapshots_content", + "metadata": "snapshots_metadata", + } + DATABASE_NAME = "persistence" + + def __init__( + self, + user_id: str, + metadata_container_access: ContainerAccess[SnapshotMetadataDocument], + content_container_access: ContainerAccess[SnapshotContentDocument], + ): + self.user_id = user_id + self.metadata_container_access = metadata_container_access + self.content_container_access = content_container_access + + async def __aenter__(self) -> "SnapshotAccess": + return self + + async def __aexit__( + self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: object | None + ) -> None: + await self.metadata_container_access.close_async() + await self.content_container_access.close_async() + + @classmethod + def create(cls, user_id: str) -> "SnapshotAccess": + metadata_container_access = ContainerAccess.create( + cls.DATABASE_NAME, cls.CONTAINER_NAMES["metadata"], SnapshotMetadataDocument + ) + content_container_access = ContainerAccess.create( + cls.DATABASE_NAME, cls.CONTAINER_NAMES["content"], SnapshotContentDocument + ) + return cls(user_id, metadata_container_access, content_container_access) + + async def get_snapshot_by_id_async(self, snapshot_id: str) -> Snapshot: + # We are accessing the content first as we only have the snapshot_id and not the user_id yet. + # The content container is partioned by snapshot_id, so we can query it directly. + # The metadata container is partitioned by user_id, so we need to query it after fetching the content. + content_document = await self.content_container_access.get_item_async( + item_id=snapshot_id, partition_key=snapshot_id + ) + metadata_document = await self.metadata_container_access.get_item_async( + item_id=snapshot_id, partition_key=content_document.owner_id + ) + + return Snapshot( + id=snapshot_id, + owner_id=content_document.owner_id, + metadata=SnapshotMetadata(**metadata_document.metadata.model_dump(mode="json", by_alias=True)), + content=content_document.content, + ) + + async def get_all_snapshots_metadata_for_user_async(self) -> List[SnapshotMetadataWithId]: + query = "SELECT * FROM c WHERE c.owner_id = @owner_id" + params = [{"name": "@owner_id", "value": self.user_id}] + items = await self.metadata_container_access.query_items_async(query=query, parameters=params) + return [self._to_metadata_summary(item) for item in items] + + async def get_filtered_snapshots_metadata_for_user_async( + self, + sort_by: Optional[SortBy] = SortBy.CREATED_AT, + sort_direction: Optional[SortDirection] = SortDirection.ASC, + limit: Optional[int] = None, + offset: Optional[int] = 0, + ) -> List[SnapshotMetadataWithId]: + if not isinstance(sort_by.value, str) or not sort_by.value.isidentifier(): + raise ServiceRequestError("Invalid sort field specified.", Service.DATABASE) + + if sort_by == SortBy.TITLE_LOWER: + metadata_array = await self.get_all_snapshots_metadata_for_user_async() + + reverse = sort_direction == SortDirection.DESC + metadata_array.sort(key=lambda s: s.title.lower() if s.title else "", reverse=reverse) + + return metadata_array[offset:] if limit is None else metadata_array[offset : offset + limit] + + offset_clause = f"OFFSET {offset} LIMIT {limit}" if limit is not None else "" + query = ( + f"SELECT * FROM c " + f"WHERE c.owner_id = @owner_id " + f"ORDER BY c.metadata.{sort_by.value} {sort_direction.value} " + offset_clause + ) + + params = [ + {"name": "@owner_id", "value": self.user_id}, + ] + + items = await self.metadata_container_access.query_items_async(query=query, parameters=params) + + return [self._to_metadata_summary(item) for item in items] + + async def get_snapshot_metadata_async(self, snapshot_id: str, owner_id: Optional[str] = None) -> SnapshotMetadata: + owner = owner_id or self.user_id + try: + document = await self.metadata_container_access.get_item_async(snapshot_id, partition_key=owner) + except CosmosResourceNotFoundError: + raise ServiceRequestError(f"Snapshot '{snapshot_id}' not found for user '{owner}'.", Service.DATABASE) + return document.metadata + + async def insert_snapshot_async(self, new_snapshot: NewSnapshot) -> str: + now = datetime.now(timezone.utc) + snapshot_id = generate(size=8) + + metadata = SnapshotMetadata( + owner_id=self.user_id, + title=new_snapshot.title, + description=new_snapshot.description, + created_at=now, + updated_at=now, + hash=hash_json_string(new_snapshot.content), + ) + + # Store metadata + await self.metadata_container_access.insert_item_async( + { + "id": snapshot_id, + "snapshot_id": snapshot_id, + "owner_id": self.user_id, + "metadata": metadata.model_dump(mode="json", by_alias=True), + } + ) + + # Store content + await self.content_container_access.insert_item_async( + { + "id": snapshot_id, + "snapshot_id": snapshot_id, + "owner_id": self.user_id, + "content": new_snapshot.content, + } + ) + + return snapshot_id + + async def delete_snapshot_async(self, snapshot_id: str): + await self._assert_ownership_async(snapshot_id) + await self.metadata_container_access.delete_item_async(snapshot_id, partition_key=self.user_id) + await self.content_container_access.delete_item_async(snapshot_id, partition_key=snapshot_id) + + async def update_snapshot_metadata_async(self, snapshot_id: str, snapshot_update: SnapshotUpdate): + existing = await self.metadata_container_access.get_item_async(snapshot_id, partition_key=self.user_id) + + updated_metadata = existing.metadata.model_copy( + update={ + "title": snapshot_update.metadata.title, + "description": snapshot_update.metadata.description, + "updated_at": datetime.now(timezone.utc), + } + ) + + await self.metadata_container_access.update_item_async( + snapshot_id, + { + "id": snapshot_id, + "snapshot_id": snapshot_id, + "owner_id": self.user_id, + "metadata": updated_metadata.model_dump(by_alias=True, mode="json"), + }, + ) + + async def _assert_ownership_async(self, snapshot_id: str) -> SnapshotMetadataDocument: + """Assert that the user owns the snapshot with the given ID.""" + try: + metadata = await self.metadata_container_access.get_item_async( + item_id=snapshot_id, partition_key=self.user_id + ) + except CosmosResourceNotFoundError: + raise ServiceRequestError( + f"Snapshot with id '{snapshot_id}' not found for user '{self.user_id}'.", Service.DATABASE + ) + + # Check if the snapshot belongs to the user - this should not be necessary if the partition key is set correctly, + # but it's a good practice to ensure the user has access. + if metadata.owner_id != self.user_id: + raise ServiceRequestError( + f"You do not have permission to access snapshot '{snapshot_id}'.", Service.DATABASE + ) + + return metadata + + @staticmethod + def _to_metadata_summary(doc: SnapshotMetadataDocument) -> SnapshotMetadataWithId: + return SnapshotMetadataWithId(**doc.metadata.model_dump(), id=doc.id) diff --git a/backend_py/primary/primary/services/database_access/snapshot_access/snapshot_logs_access.py b/backend_py/primary/primary/services/database_access/snapshot_access/snapshot_logs_access.py new file mode 100644 index 000000000..e870587b0 --- /dev/null +++ b/backend_py/primary/primary/services/database_access/snapshot_access/snapshot_logs_access.py @@ -0,0 +1,98 @@ +import logging +from datetime import datetime, timezone + + +from primary.services.database_access.container_access import ContainerAccess +from primary.services.service_exceptions import ServiceRequestError + +from .query_collation_options import QueryCollationOptions +from .models import SnapshotAccessLog +from .util import make_access_log_item_id + + +LOGGER = logging.getLogger(__name__) + + +class SnapshotLogsAccess: + DATABASE_NAME = "persistence" + CONTAINER_NAME = "snapshot_access_log" + + def __init__(self, user_id: str, container_access: ContainerAccess[SnapshotAccessLog]): + self._user_id = user_id + self._container_access = container_access + + @classmethod + def create(cls, user_id: str) -> "SnapshotLogsAccess": + container_access = ContainerAccess.create(cls.DATABASE_NAME, cls.CONTAINER_NAME, SnapshotAccessLog) + + return cls(user_id, container_access) + + async def __aenter__(self) -> "SnapshotLogsAccess": + return self + + async def __aexit__( + self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: object | None + ) -> None: + # Clean up if needed (e.g., closing DB connections) + await self._container_access.close_async() + + async def get_access_logs_for_user_async(self, collation_options: QueryCollationOptions) -> list[SnapshotAccessLog]: + query = "SELECT * FROM c WHERE c.visitor_id = @visitor_id" + params = [{"name": "@visitor_id", "value": self._user_id}] + + search_options = collation_options.to_sql_query_string("c") + + if search_options: + query = f"{query} {search_options}" + + return await self._container_access.query_items_async(query, params) # type: ignore[arg-type] + + async def create_access_log_async(self, snapshot_id: str, snapshot_owner_id: str) -> SnapshotAccessLog: + new_log = SnapshotAccessLog( + visitor_id=self._user_id, + snapshot_id=snapshot_id, + snapshot_owner_id=snapshot_owner_id, + ) + + _inserted_id = await self._container_access.insert_item_async(new_log) + + return new_log + + async def get_access_log_async(self, snapshot_id: str) -> SnapshotAccessLog: + item_id = make_access_log_item_id(snapshot_id, self._user_id) + + return await self._container_access.get_item_async(item_id, partition_key=self._user_id) + + async def get_existing_or_new_log_item_async(self, snapshot_id: str, snapshot_owner_id: str) -> SnapshotAccessLog: + """ + Returns an already stored log item if it exists, otherwise, creates a new instance. + + **Note: This does create a new entry in the database!** + """ + try: + return await self.get_access_log_async(snapshot_id) + except ServiceRequestError: + return SnapshotAccessLog( + visitor_id=self._user_id, + snapshot_id=snapshot_id, + snapshot_owner_id=snapshot_owner_id, + ) + + async def log_snapshot_visit_async(self, snapshot_id: str, snapshot_owner_id: str) -> SnapshotAccessLog: + timestamp = datetime.now(timezone.utc) + + # Should we wrap this? + # try: + # + # except Exception as e: + # raise ServiceRequestError(f"Failed to log snapshot visit: {str(e)}", Service.DATABASE) from e + log = await self.get_existing_or_new_log_item_async(snapshot_id, snapshot_owner_id) + log.visits += 1 + log.last_visited_at = timestamp + + if not log.first_visited_at: + log.first_visited_at = timestamp + + await self._container_access.update_item_async(log.id, log) + + return log diff --git a/backend_py/primary/primary/services/database_access/snapshot_access/types.py b/backend_py/primary/primary/services/database_access/snapshot_access/types.py new file mode 100644 index 000000000..add26db1a --- /dev/null +++ b/backend_py/primary/primary/services/database_access/snapshot_access/types.py @@ -0,0 +1,54 @@ +from typing import Optional +from datetime import datetime +from enum import Enum +from pydantic import BaseModel + + +class SnapshotUserEditableMetadata(BaseModel): + title: str + description: Optional[str] = None + + +class SnapshotMetadataInternal(BaseModel): + owner_id: str + created_at: datetime + updated_at: datetime + hash: str + + +class SnapshotMetadata(SnapshotUserEditableMetadata, SnapshotMetadataInternal): + pass + + +class Snapshot(BaseModel): + id: str + owner_id: str + metadata: SnapshotMetadata + content: str + + +class SnapshotMetadataWithId(SnapshotMetadata): + id: str + + +class SnapshotUpdate(BaseModel): + metadata: SnapshotUserEditableMetadata + + +class NewSnapshot(BaseModel): + title: str + description: Optional[str] + content: str + + +class SortBy(str, Enum): + CREATED_AT = "created_at" + UPDATED_AT = "updated_at" + TITLE = "title" + TITLE_LOWER = "title_lower" + LAST_VISIT = "last_visited_at" + + +class SortDirection(str, Enum): + ASC = "asc" + DESC = "desc" diff --git a/backend_py/primary/primary/services/database_access/snapshot_access/util.py b/backend_py/primary/primary/services/database_access/snapshot_access/util.py new file mode 100644 index 000000000..837a8e71f --- /dev/null +++ b/backend_py/primary/primary/services/database_access/snapshot_access/util.py @@ -0,0 +1,2 @@ +def make_access_log_item_id(snapshot_id: str, visitor_id: str) -> str: + return f"{snapshot_id}__{visitor_id}" diff --git a/backend_py/primary/primary/services/service_exceptions.py b/backend_py/primary/primary/services/service_exceptions.py index 673d1d73f..aad95adee 100644 --- a/backend_py/primary/primary/services/service_exceptions.py +++ b/backend_py/primary/primary/services/service_exceptions.py @@ -8,6 +8,7 @@ class Service(str, Enum): VDS = "vds" USER_SESSION = "user_session" SSDL = "ssdl" + DATABASE = "database" class ServiceLayerException(Exception): diff --git a/backend_py/primary/pyproject.toml b/backend_py/primary/pyproject.toml index 8e0fc3a2f..a364aa87f 100644 --- a/backend_py/primary/pyproject.toml +++ b/backend_py/primary/pyproject.toml @@ -35,6 +35,9 @@ core_utils = { path = "../libs/core_utils", develop = true } server_schemas = { path = "../libs/server_schemas", develop = true } polars = "^1.6.0" fmu-datamodels = "0.0.1" +azure-cosmos = "^4.9.0" +aiohttp = "^3.11.18" +nanoid = "^2.0.0" [tool.poetry.group.dev.dependencies] black = "^25.1.0" diff --git a/docker-compose.yml b/docker-compose.yml index faabb1500..baf0f84c0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,96 +2,123 @@ # for doing development of the frontend + backend in combination. services: - frontend: - build: - context: . - dockerfile: frontend-dev.Dockerfile - ports: - - 8080:8080 - volumes: - - ./frontend/public:/usr/src/app/frontend/public - - ./frontend/src:/usr/src/app/frontend/src - - ./frontend/theme:/usr/src/app/frontend/theme - - ./frontend/index.html:/usr/src/app/frontend/index.html + frontend: + build: + context: . + dockerfile: frontend-dev.Dockerfile + ports: + - 8080:8080 + volumes: + - ./frontend/public:/usr/src/app/frontend/public + - ./frontend/src:/usr/src/app/frontend/src + - ./frontend/theme:/usr/src/app/frontend/theme + - ./frontend/index.html:/usr/src/app/frontend/index.html - backend-primary: - build: - context: . - dockerfile: ./backend_py/primary/Dockerfile - ports: - - 5000:5000 - - 5678:5678 - environment: - - UVICORN_PORT=5000 - - UVICORN_RELOAD=true - - WEBVIZ_CLIENT_SECRET - - WEBVIZ_SMDA_RESOURCE_SCOPE - - WEBVIZ_SMDA_SUBSCRIPTION_KEY - - WEBVIZ_ENTERPRISE_SUBSCRIPTION_KEY - - WEBVIZ_SSDL_RESOURCE_SCOPE - - WEBVIZ_SUMO_ENV - - WEBVIZ_VDS_HOST_ADDRESS - - APPLICATIONINSIGHTS_CONNECTION_STRING - - OTEL_RESOURCE_ATTRIBUTES=service.name=primary-backend, service.namespace=local - - CODESPACE_NAME # Automatically set env. variable by GitHub codespace - volumes: - - ./backend_py/primary/primary:/home/appuser/backend_py/primary/primary - - ./backend_py/libs:/home/appuser/backend_py/libs - command: - [ - "sh", - "-c", - "pip install debugpy && python -m debugpy --listen 0.0.0.0:5678 -m uvicorn --proxy-headers --host=0.0.0.0 primary.main:app", - ] + backend-primary: + depends_on: + cosmos-db: + condition: service_healthy + build: + context: . + dockerfile: ./backend_py/primary/Dockerfile + ports: + - 5000:5000 + - 5678:5678 + environment: + - UVICORN_PORT=5000 + - UVICORN_RELOAD=true + - WEBVIZ_CLIENT_SECRET + - WEBVIZ_SMDA_RESOURCE_SCOPE + - WEBVIZ_SMDA_SUBSCRIPTION_KEY + - WEBVIZ_ENTERPRISE_SUBSCRIPTION_KEY + - WEBVIZ_SSDL_RESOURCE_SCOPE + - WEBVIZ_SUMO_ENV + - WEBVIZ_VDS_HOST_ADDRESS + - APPLICATIONINSIGHTS_CONNECTION_STRING + - OTEL_RESOURCE_ATTRIBUTES=service.name=primary-backend, service.namespace=local + - CODESPACE_NAME # Automatically set env. variable by GitHub codespace + - WEBVIZ_DB_CONNECTION_STRING + volumes: + - ./backend_py/primary/primary:/home/appuser/backend_py/primary/primary + - ./backend_py/libs:/home/appuser/backend_py/libs + command: + [ + "sh", + "-c", + "pip install debugpy && python -m debugpy --listen 0.0.0.0:5678 -m uvicorn --proxy-headers --host=0.0.0.0 primary.main:app", + ] - surface-query: - build: - context: . - dockerfile: ./backend_go/surface-query-dev.Dockerfile - ports: - - 5001:5001 - volumes: - - ./backend_go/surface_query:/home/appuser/backend_go/surface_query + surface-query: + build: + context: . + dockerfile: ./backend_go/surface-query-dev.Dockerfile + ports: + - 5001:5001 + volumes: + - ./backend_go/surface_query:/home/appuser/backend_go/surface_query - user-mock: - build: - context: . - dockerfile: ./backend_py/user_mock/Dockerfile - ports: - - 8001:8001 - environment: - - UVICORN_PORT=8001 - - UVICORN_RELOAD=true - volumes: - - ./backend_py/user_mock/user_mock:/home/appuser/backend_py/user_mock/user_mock + user-mock: + build: + context: . + dockerfile: ./backend_py/user_mock/Dockerfile + ports: + - 8001:8001 + environment: + - UVICORN_PORT=8001 + - UVICORN_RELOAD=true + volumes: + - ./backend_py/user_mock/user_mock:/home/appuser/backend_py/user_mock/user_mock - user-grid3d-ri: - build: - context: . - dockerfile: ./backend_py/user_grid3d_ri/Dockerfile - ports: - - 8002:8002 - environment: - - UVICORN_PORT=8002 - - UVICORN_RELOAD=true - - APPLICATIONINSIGHTS_CONNECTION_STRING - - OTEL_RESOURCE_ATTRIBUTES=service.name=user-grid3d-ri, service.namespace=local - volumes: - - ./backend_py/user_grid3d_ri/user_grid3d_ri:/home/appuser/backend_py/user_grid3d_ri/user_grid3d_ri - - ./backend_py/libs:/home/appuser/backend_py/libs + user-grid3d-ri: + build: + context: . + dockerfile: ./backend_py/user_grid3d_ri/Dockerfile + ports: + - 8002:8002 + environment: + - UVICORN_PORT=8002 + - UVICORN_RELOAD=true + - APPLICATIONINSIGHTS_CONNECTION_STRING + - OTEL_RESOURCE_ATTRIBUTES=service.name=user-grid3d-ri, service.namespace=local + volumes: + - ./backend_py/user_grid3d_ri/user_grid3d_ri:/home/appuser/backend_py/user_grid3d_ri/user_grid3d_ri + - ./backend_py/libs:/home/appuser/backend_py/libs - redis-user-session: - image: bitnami/redis:6.2.10@sha256:bd42fcdab5959ce2b21b6ea8410d4b3ee87ecb2e320260326ec731ecfcffbd0e - expose: - - 6379 - environment: - - ALLOW_EMPTY_PASSWORD=yes + redis-user-session: + image: bitnami/redis:6.2.10@sha256:bd42fcdab5959ce2b21b6ea8410d4b3ee87ecb2e320260326ec731ecfcffbd0e + expose: + - 6379 + environment: + - ALLOW_EMPTY_PASSWORD=yes - redis-cache: - image: bitnami/redis:6.2.10@sha256:bd42fcdab5959ce2b21b6ea8410d4b3ee87ecb2e320260326ec731ecfcffbd0e - expose: - - 6379 - environment: - - ALLOW_EMPTY_PASSWORD=yes - # https://redis.io/docs/management/config/#configuring-redis-as-a-cache - - REDIS_EXTRA_FLAGS=--maxmemory 1gb --maxmemory-policy allkeys-lru --save '' --appendonly no --loglevel notice + redis-cache: + image: bitnami/redis:6.2.10@sha256:bd42fcdab5959ce2b21b6ea8410d4b3ee87ecb2e320260326ec731ecfcffbd0e + expose: + - 6379 + environment: + - ALLOW_EMPTY_PASSWORD=yes + # https://redis.io/docs/management/config/#configuring-redis-as-a-cache + - REDIS_EXTRA_FLAGS=--maxmemory 1gb --maxmemory-policy allkeys-lru --save '' --appendonly no --loglevel notice + + cosmos-db: + image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest + platform: linux/amd64 + tty: true + mem_limit: 4GB + ports: + - "8081:8081" # HTTPS endpoint + - "10250-10255:10250-10255" # Internal emulator ports (needed) + environment: + - AZURE_COSMOS_EMULATOR_PARTITION_COUNT=1 + - AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=true + healthcheck: + test: curl -k https://127.0.0.1:8081/_explorer/emulator.pem || exit 1 + interval: 10s + retries: 12 # 2 minutes + start_period: 30s + timeout: 5s + volumes: + - cosmosdrive:/var/lib/cosmosdb/emulator + +volumes: + cosmosdrive: diff --git a/frontend/aliases.json b/frontend/aliases.json index 2782caac6..66b364e81 100644 --- a/frontend/aliases.json +++ b/frontend/aliases.json @@ -7,7 +7,8 @@ "@framework/*": ["./src/framework/*"], "@lib/*": ["./src/lib/*"], "@modules/*": ["./src/modules/*"], - "@modules_shared/*": ["./src/modules/_shared/*"] + "@modules_shared/*": ["./src/modules/_shared/*"], + "@src/*": ["./src/*"] } } } diff --git a/frontend/index.html b/frontend/index.html index 80a372409..e7eeafea2 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -9,7 +9,7 @@- Disclaimer: Webviz is a service provided by Equinor and is not a way of sharing - official data. Data should continue to be shared through L2S, FTP and/or Dasha. -
-- References to e.g. earlier models, model results and data should still be done through the mentioned - tools, and not Webviz. Since Webviz is currently under heavy development and not production ready, there - is no guarantee given as of now that calculations are error-free. -
-Please sign in to continue.
- -